mirror of
https://github.com/httpie/cli.git
synced 2024-12-26 16:29:00 +01:00
Allow embeding text (=@) and JSON (:=@) files content into request data fields.
This commit is contained in:
parent
54c5c3d82b
commit
d5bc564e4f
41
README.rst
41
README.rst
@ -246,14 +246,15 @@ command:
|
||||
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
|
||||
form data, files, and URL parameters.
|
||||
|
||||
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
|
||||
their type is distinguished only by the separator used:
|
||||
``:``, ``=``, ``:=``, ``@``, and ``==``.
|
||||
``:``, ``=``, ``:=``, ``==``, ``@``, ``=@``, and ``:=@``. The ones with an
|
||||
``@`` expect a file path as value.
|
||||
|
||||
+-----------------------+-----------------------------------------------------+
|
||||
| Item Type | Description |
|
||||
@ -266,16 +267,16 @@ their type is distinguished only by the separator used:
|
||||
| | The ``==`` separator is used |
|
||||
+-----------------------+-----------------------------------------------------+
|
||||
| Data Fields | Request data fields to be serialized as a JSON |
|
||||
| ``field=value`` | object (default), or to be form encoded |
|
||||
| | (``--form, -f``). |
|
||||
| ``field=value``, | object (default), or to be form encoded |
|
||||
| ``field=@file.txt`` | (``--form, -f``). |
|
||||
+-----------------------+-----------------------------------------------------+
|
||||
| Raw JSON fields | Useful when sending JSON and one or |
|
||||
| ``field:=json`` | more fields need to be a ``Boolean``, ``Number``, |
|
||||
| | nested ``Object``, or an ``Array``, e.g., |
|
||||
| ``field:=json``, | more fields need to be a ``Boolean``, ``Number``, |
|
||||
| ``field:=@file.json`` | nested ``Object``, or an ``Array``, e.g., |
|
||||
| | ``meals:='["ham","spam"]'`` or ``pies:=[1,2,3]`` |
|
||||
| | (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``. |
|
||||
| | The presence of a file field results |
|
||||
| | in a ``multipart/form-data`` request. |
|
||||
@ -285,6 +286,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
|
||||
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:
|
||||
`Redirected input`_ allows for passing arbitrary data to be sent with the
|
||||
request.
|
||||
@ -332,11 +335,16 @@ Simple example:
|
||||
|
||||
|
||||
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
|
||||
|
||||
$ 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
|
||||
@ -352,8 +360,12 @@ into the resulting object:
|
||||
"http",
|
||||
"pies"
|
||||
],
|
||||
"description": "John is a nice guy who likes pies.",
|
||||
"married": false,
|
||||
"name": "John"
|
||||
"name": "John",
|
||||
"bookmarks": {
|
||||
"HTTPie": "http://httpie.org",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -383,7 +395,7 @@ Regular Forms
|
||||
|
||||
.. 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
|
||||
@ -391,7 +403,7 @@ Regular Forms
|
||||
POST /person/1 HTTP/1.1
|
||||
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 +428,9 @@ submitted:
|
||||
<input type="file" name="cv" />
|
||||
</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
|
||||
@ -1214,6 +1229,8 @@ Changelog
|
||||
*You can click a version name to see a diff with the previous one.*
|
||||
|
||||
* `0.8.0-dev`_
|
||||
* Added ``field=@file.txt`` and ``field:=@file.json`` for embedding
|
||||
the contents of text and JSON files into request data.
|
||||
* `0.7.1`_ (2013-09-24)
|
||||
* Added ``--ignore-stdin``.
|
||||
* Added support for auth plugins.
|
||||
|
@ -3,7 +3,7 @@ HTTPie - a CLI, cURL-like tool for humans.
|
||||
|
||||
"""
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__version__ = '0.7.1'
|
||||
__version__ = '0.8.0-dev'
|
||||
__licence__ = 'BSD'
|
||||
|
||||
|
||||
|
@ -15,7 +15,7 @@ from .plugins import plugin_manager
|
||||
from .sessions import DEFAULT_SESSIONS_DIR
|
||||
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
||||
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_RESP_BODY, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
|
||||
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator)
|
||||
@ -94,7 +94,7 @@ positional.add_argument(
|
||||
'items',
|
||||
metavar='REQUEST ITEM',
|
||||
nargs=ZERO_OR_MORE,
|
||||
type=KeyValueArgType(*SEP_GROUP_ITEMS),
|
||||
type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS),
|
||||
help=r"""
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
determines the type:
|
||||
@ -112,13 +112,21 @@ positional.add_argument(
|
||||
|
||||
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):
|
||||
|
||||
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:
|
||||
|
||||
|
@ -37,22 +37,40 @@ SEP_PROXY = ':'
|
||||
SEP_DATA = '='
|
||||
SEP_DATA_RAW_JSON = ':='
|
||||
SEP_FILES = '@'
|
||||
SEP_DATA_EMBED_FILE = '=@'
|
||||
SEP_DATA_EMBED_RAW_JSON_FILE = ':=@'
|
||||
SEP_QUERY = '=='
|
||||
|
||||
# Separators that become request data
|
||||
SEP_GROUP_DATA_ITEMS = frozenset([
|
||||
SEP_DATA,
|
||||
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
|
||||
SEP_GROUP_ITEMS = frozenset([
|
||||
SEP_GROUP_ALL_ITEMS = frozenset([
|
||||
SEP_HEADERS,
|
||||
SEP_QUERY,
|
||||
SEP_DATA,
|
||||
SEP_DATA_RAW_JSON,
|
||||
SEP_FILES
|
||||
SEP_FILES,
|
||||
SEP_DATA_EMBED_FILE,
|
||||
SEP_DATA_EMBED_RAW_JSON_FILE,
|
||||
])
|
||||
|
||||
|
||||
@ -257,7 +275,7 @@ class Parser(ArgumentParser):
|
||||
# Parse the URL as an ITEM and store it as the first ITEM arg.
|
||||
self.args.items.insert(
|
||||
0,
|
||||
KeyValueArgType(*SEP_GROUP_ITEMS).__call__(self.args.url)
|
||||
KeyValueArgType(*SEP_GROUP_ALL_ITEMS).__call__(self.args.url)
|
||||
)
|
||||
|
||||
except ArgumentTypeError as e:
|
||||
@ -558,9 +576,7 @@ def parse_items(items, data=None, headers=None, files=None, params=None):
|
||||
params = ParamDict()
|
||||
|
||||
for item in items:
|
||||
|
||||
value = item.value
|
||||
key = item.key
|
||||
|
||||
if item.sep == SEP_HEADERS:
|
||||
target = headers
|
||||
@ -572,21 +588,28 @@ def parse_items(items, data=None, headers=None, files=None, params=None):
|
||||
value = (os.path.basename(value),
|
||||
BytesIO(f.read()))
|
||||
except IOError as e:
|
||||
raise ParseError(
|
||||
'Invalid argument "%s": %s' % (item.orig, e))
|
||||
raise ParseError('"%s": %s' % (item.orig, e))
|
||||
target = files
|
||||
|
||||
elif item.sep in [SEP_DATA, SEP_DATA_RAW_JSON]:
|
||||
if item.sep == SEP_DATA_RAW_JSON:
|
||||
elif item.sep in SEP_GROUP_DATA_ITEMS:
|
||||
|
||||
if item.sep in SEP_GROUP_DATA_EMBED_ITEMS:
|
||||
try:
|
||||
value = json.loads(item.value)
|
||||
except ValueError:
|
||||
raise ParseError('"%s" is not valid JSON' % item.orig)
|
||||
with open(os.path.expanduser(value), 'rb') as f:
|
||||
value = f.read()
|
||||
except IOError as e:
|
||||
raise ParseError('%s": %s' % (item.orig, e))
|
||||
|
||||
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
|
||||
|
||||
else:
|
||||
raise TypeError(item)
|
||||
|
||||
target[key] = value
|
||||
target[item.key] = value
|
||||
|
||||
return headers, data, files, params
|
||||
|
3
tests/fixtures/test.json
vendored
Normal file
3
tests/fixtures/test.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"hello": "world"
|
||||
}
|
@ -101,15 +101,22 @@ def patharg(path):
|
||||
FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.txt')
|
||||
FILE2_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file2.txt')
|
||||
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)
|
||||
FILE2_PATH_ARG = patharg(FILE2_PATH)
|
||||
BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH)
|
||||
JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH)
|
||||
|
||||
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()
|
||||
with open(BIN_FILE_PATH, 'rb') as f:
|
||||
BIN_FILE_CONTENT = f.read()
|
||||
with open(JSON_FILE_PATH, 'rb') as f:
|
||||
JSON_FILE_CONTENT = f.read()
|
||||
|
||||
|
||||
def httpbin(path):
|
||||
@ -1172,11 +1179,7 @@ class ItemParsingTest(BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.key_value_type = input.KeyValueArgType(
|
||||
input.SEP_HEADERS,
|
||||
input.SEP_QUERY,
|
||||
input.SEP_DATA,
|
||||
input.SEP_DATA_RAW_JSON,
|
||||
input.SEP_FILES,
|
||||
*input.SEP_GROUP_ALL_ITEMS
|
||||
)
|
||||
|
||||
def test_invalid_items(self):
|
||||
@ -1223,26 +1226,41 @@ class ItemParsingTest(BaseTestCase):
|
||||
self.key_value_type('eh:'),
|
||||
self.key_value_type('ed='),
|
||||
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('string-embed=@' + FILE_PATH_ARG),
|
||||
self.key_value_type('raw-json-embed:=@' + JSON_FILE_PATH_ARG),
|
||||
])
|
||||
|
||||
# Parsed headers
|
||||
# `requests.structures.CaseInsensitiveDict` => `dict`
|
||||
headers = dict(headers._store.values())
|
||||
self.assertDictEqual(headers, {
|
||||
'header': 'value',
|
||||
'eh': ''
|
||||
})
|
||||
self.assertDictEqual(data, {
|
||||
|
||||
# Parsed data
|
||||
raw_json_embed = data.pop('raw-json-embed')
|
||||
self.assertDictEqual(raw_json_embed, json.loads(JSON_FILE_CONTENT))
|
||||
data['string-embed'] = data['string-embed'].strip()
|
||||
self.assertDictEqual(dict(data), {
|
||||
"ed": "",
|
||||
"string": "value",
|
||||
"bool": True,
|
||||
"list": ["a", 1, {}, False],
|
||||
"obj": {"a": "b"},
|
||||
"string-embed": FILE_CONTENT,
|
||||
})
|
||||
|
||||
# Parsed query string parameters
|
||||
self.assertDictEqual(params, {
|
||||
'query': 'value',
|
||||
})
|
||||
self.assertIn('test-file', files)
|
||||
|
||||
# Parsed file fields
|
||||
self.assertIn('file', files)
|
||||
self.assertEqual(files['file'][1].read().strip(), FILE_CONTENT)
|
||||
|
||||
|
||||
class ArgumentParserTestCase(unittest.TestCase):
|
||||
|
Loading…
Reference in New Issue
Block a user