mirror of
https://github.com/httpie/cli.git
synced 2025-02-18 02:20:51 +01:00
Added file upload support
It is now possible to send multipart/form-data requests. Note that the --file option used previously has been removed because it didn't allow you specify the field name. Example: http -f POST example.com field-name@/path/to/file
This commit is contained in:
parent
578acacdf3
commit
b7e0473d6c
@ -4,7 +4,7 @@ python:
|
|||||||
- 2.7
|
- 2.7
|
||||||
# TODO: Python 3
|
# TODO: Python 3
|
||||||
#- 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' ]]; then pip install argparse; fi"
|
||||||
|
50
README.rst
50
README.rst
@ -40,15 +40,18 @@ Synopsis::
|
|||||||
|
|
||||||
http [flags] METHOD URL [items]
|
http [flags] METHOD URL [items]
|
||||||
|
|
||||||
There are three types of key-value pair items available:
|
There are four types of key-value pair items available:
|
||||||
|
|
||||||
Headers
|
Headers
|
||||||
Arbitrary HTTP headers. The ``:`` character is used to separate a header's name from its value, e.g., ``X-API-Token:123``.
|
Arbitrary HTTP headers. The ``:`` character is used to separate a header's name from its value, e.g., ``X-API-Token:123``.
|
||||||
|
|
||||||
Simple data items
|
Simple data fields
|
||||||
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``.
|
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 items
|
File fields
|
||||||
|
Only available with ``-f`` / ``--form``. Use ``@`` as the separator, e.g., ``screenshot@/path/to/file.png``.
|
||||||
|
|
||||||
|
Raw JSON fields
|
||||||
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]``.
|
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]``.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
@ -77,6 +80,17 @@ It can easily be changed to a 'form' request using the ``-f`` (or ``--form``) fl
|
|||||||
|
|
||||||
age=29&name=John&email=john%40example.org
|
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/job-application email=John cv@~/Documents/cv.pdf
|
||||||
|
|
||||||
|
The above will send a request equivalent to submitting the following form::
|
||||||
|
|
||||||
|
<form enctype="multipart/form-data" method="post" action="http://example.com/job-application" >
|
||||||
|
<input type="text" name="email" />
|
||||||
|
<input type="file" name="cv" />
|
||||||
|
</form>
|
||||||
|
|
||||||
A whole request body can be passed in via ``stdin`` instead::
|
A whole request body can be passed in via ``stdin`` instead::
|
||||||
|
|
||||||
echo '{"name": "John"}' | http PATCH example.com/person/1 X-API-Token:123
|
echo '{"name": "John"}' | http PATCH example.com/person/1 X-API-Token:123
|
||||||
@ -88,14 +102,6 @@ Flags
|
|||||||
^^^^^
|
^^^^^
|
||||||
Most of the flags mirror the arguments understood by ``requests.request``. See ``http -h`` for more details::
|
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 | --headers | --body]
|
|
||||||
[--style STYLE] [--auth AUTH] [--verify VERIFY]
|
|
||||||
[--proxy PROXY] [--allow-redirects] [--file PATH]
|
|
||||||
[--timeout TIMEOUT]
|
|
||||||
METHOD URL [items [items ...]]
|
|
||||||
|
|
||||||
HTTPie - cURL for humans.
|
HTTPie - cURL for humans.
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
@ -103,17 +109,20 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
|
|||||||
PUT, DELETE, PATCH, ...).
|
PUT, DELETE, PATCH, ...).
|
||||||
URL Protocol defaults to http:// if the URL does not
|
URL Protocol defaults to http:// if the URL does not
|
||||||
include it.
|
include it.
|
||||||
items HTTP header (key:value), data field (key=value) or raw
|
items HTTP header (header:value), data field (field=value),
|
||||||
JSON field (field:=value).
|
raw JSON field (field:=value) or file field
|
||||||
|
(field@/path/to/file).
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
--json, -j Serialize data items as a JSON object and set Content-
|
--json, -j Serialize data items as a JSON object and set Content-
|
||||||
Type to application/json, if not specified.
|
Type to application/json, if not specified.
|
||||||
--form, -f Serialize data items as form values and set Content-
|
--form, -f Serialize fields as form values. The Content-Type is
|
||||||
Type to application/x-www-form-urlencoded, if not
|
set to application/x-www-form-urlencoded. The presence
|
||||||
specified.
|
of any file fields results into a multipart/form-data
|
||||||
|
request. Note that Content-Type is not automatically
|
||||||
|
set if explicitely specified.
|
||||||
--traceback Print exception traceback should one occur.
|
--traceback Print exception traceback should one occur.
|
||||||
--pretty If stdout is a terminal, the response is prettified by
|
--pretty If stdout is a terminal, the response is prettified by
|
||||||
default (colorized and indented if it is JSON). This
|
default (colorized and indented if it is JSON). This
|
||||||
@ -126,10 +135,11 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
|
|||||||
"h" stands for response headers and "b" for response
|
"h" stands for response headers and "b" for response
|
||||||
body. Defaults to "hb" which means that the whole
|
body. Defaults to "hb" which means that the whole
|
||||||
response (headers and body) is printed.
|
response (headers and body) is printed.
|
||||||
--headers, -t Print only the response headers. It's a shortcut for
|
--verbose, -v Print the whole request as well as response. Shortcut
|
||||||
|
for --print=HBhb.
|
||||||
|
--headers, -t Print only the response headers. Shortcut for
|
||||||
--print=h.
|
--print=h.
|
||||||
--body, -b Print only the response body. It's a shortcut for
|
--body, -b Print only the response body. Shortcut for --print=b.
|
||||||
--print=b.
|
|
||||||
--style STYLE, -s STYLE
|
--style STYLE, -s STYLE
|
||||||
Output coloring style, one of autumn, borland, bw,
|
Output coloring style, one of autumn, borland, bw,
|
||||||
colorful, default, emacs, friendly, fruity, manni,
|
colorful, default, emacs, friendly, fruity, manni,
|
||||||
@ -144,11 +154,9 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
|
|||||||
http:foo.bar:3128).
|
http:foo.bar:3128).
|
||||||
--allow-redirects Set this flag if full redirects are allowed (e.g. re-
|
--allow-redirects Set this flag if full redirects are allowed (e.g. re-
|
||||||
POST-ing of data at new ``Location``)
|
POST-ing of data at new ``Location``)
|
||||||
--file PATH File to multipart upload
|
|
||||||
--timeout TIMEOUT Float describes the timeout of the request (Use
|
--timeout TIMEOUT Float describes the timeout of the request (Use
|
||||||
socket.setdefaulttimeout() as fallback).
|
socket.setdefaulttimeout() as fallback).
|
||||||
|
|
||||||
|
|
||||||
Contributors
|
Contributors
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -100,26 +100,37 @@ def main(args=None,
|
|||||||
headers = CaseInsensitiveDict()
|
headers = CaseInsensitiveDict()
|
||||||
headers['User-Agent'] = DEFAULT_UA
|
headers['User-Agent'] = DEFAULT_UA
|
||||||
data = OrderedDict()
|
data = OrderedDict()
|
||||||
|
files = OrderedDict()
|
||||||
try:
|
try:
|
||||||
cli.parse_items(items=args.items, headers=headers, data=data)
|
cli.parse_items(items=args.items, headers=headers,
|
||||||
|
data=data, files=files)
|
||||||
except cli.ParseError as e:
|
except cli.ParseError as e:
|
||||||
if args.traceback:
|
if args.traceback:
|
||||||
raise
|
raise
|
||||||
parser.error(e.message)
|
parser.error(e.message)
|
||||||
|
|
||||||
|
if files and not args.form:
|
||||||
|
# We could just switch to --form automatically here,
|
||||||
|
# but I think it's better to make it explicit.
|
||||||
|
parser.error(
|
||||||
|
' You need to set the --form / -f flag to'
|
||||||
|
' to issue a multipart request. File fields: %s'
|
||||||
|
% ','.join(files.keys()))
|
||||||
|
|
||||||
if not stdin_isatty:
|
if not stdin_isatty:
|
||||||
if data:
|
if 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()
|
data = stdin.read()
|
||||||
|
|
||||||
|
|
||||||
# JSON/Form content type.
|
# JSON/Form content type.
|
||||||
if args.json or (not args.form and data):
|
if args.json or (not args.form and data):
|
||||||
if stdin_isatty:
|
if stdin_isatty:
|
||||||
data = json.dumps(data)
|
data = json.dumps(data)
|
||||||
if 'Content-Type' not in headers and (data or args.json):
|
if not files and ('Content-Type' not in headers and (data or args.json)):
|
||||||
headers['Content-Type'] = TYPE_JSON
|
headers['Content-Type'] = TYPE_JSON
|
||||||
elif 'Content-Type' not in headers:
|
elif not files and 'Content-Type' not in headers:
|
||||||
headers['Content-Type'] = TYPE_FORM
|
headers['Content-Type'] = TYPE_FORM
|
||||||
|
|
||||||
# Fire the request.
|
# Fire the request.
|
||||||
@ -133,7 +144,7 @@ def main(args=None,
|
|||||||
timeout=args.timeout,
|
timeout=args.timeout,
|
||||||
auth=(args.auth.key, args.auth.value) if args.auth else None,
|
auth=(args.auth.key, args.auth.value) if args.auth else None,
|
||||||
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=files,
|
||||||
allow_redirects=args.allow_redirects,
|
allow_redirects=args.allow_redirects,
|
||||||
)
|
)
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
@ -10,6 +11,7 @@ SEP_COMMON = ':'
|
|||||||
SEP_HEADERS = SEP_COMMON
|
SEP_HEADERS = SEP_COMMON
|
||||||
SEP_DATA = '='
|
SEP_DATA = '='
|
||||||
SEP_DATA_RAW_JSON = ':='
|
SEP_DATA_RAW_JSON = ':='
|
||||||
|
SEP_FILES = '@'
|
||||||
PRETTIFY_STDOUT_TTY_ONLY = object()
|
PRETTIFY_STDOUT_TTY_ONLY = object()
|
||||||
|
|
||||||
OUT_REQUEST_HEADERS = 'H'
|
OUT_REQUEST_HEADERS = 'H'
|
||||||
@ -49,16 +51,28 @@ class KeyValueType(object):
|
|||||||
return KeyValue(key=key, value=value, sep=sep, orig=string)
|
return KeyValue(key=key, value=value, sep=sep, orig=string)
|
||||||
|
|
||||||
|
|
||||||
def parse_items(items, data=None, headers=None):
|
def parse_items(items, data=None, headers=None, files=None):
|
||||||
"""Parse `KeyValueType` `items` into `data` and `headers`."""
|
"""Parse `KeyValueType` `items` into `data`, `headers` and `files`."""
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
if data is None:
|
if data is None:
|
||||||
data = {}
|
data = {}
|
||||||
|
if files is None:
|
||||||
|
files = {}
|
||||||
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
|
||||||
|
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]:
|
elif item.sep in [SEP_DATA, SEP_DATA_RAW_JSON]:
|
||||||
if item.sep == SEP_DATA_RAW_JSON:
|
if item.sep == SEP_DATA_RAW_JSON:
|
||||||
try:
|
try:
|
||||||
@ -69,12 +83,12 @@ def parse_items(items, data=None, headers=None):
|
|||||||
else:
|
else:
|
||||||
raise ParseError('%s is not valid item' % item.orig)
|
raise ParseError('%s is not valid item' % item.orig)
|
||||||
|
|
||||||
if item.key in target:
|
if key in target:
|
||||||
ParseError('duplicate item %s (%s)' % (item.key, item.orig))
|
ParseError('duplicate item %s (%s)' % (item.key, item.orig))
|
||||||
|
|
||||||
target[item.key] = value
|
target[key] = value
|
||||||
|
|
||||||
return headers, data
|
return headers, data, files
|
||||||
|
|
||||||
|
|
||||||
def _(text):
|
def _(text):
|
||||||
@ -111,9 +125,9 @@ group_type.add_argument(
|
|||||||
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
|
Serialize fields as form values. The Content-Type is set to application/x-www-form-urlencoded.
|
||||||
Content-Type to application/x-www-form-urlencoded,
|
The presence of any file fields results into a multipart/form-data request.
|
||||||
if not specified.
|
Note that Content-Type is not automatically set if explicitely specified.
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -225,11 +239,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=_('''
|
||||||
@ -258,9 +267,10 @@ parser.add_argument(
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'items', nargs='*',
|
'items', nargs='*',
|
||||||
type=KeyValueType(SEP_COMMON, SEP_DATA, SEP_DATA_RAW_JSON),
|
type=KeyValueType(SEP_COMMON, SEP_DATA, SEP_DATA_RAW_JSON, SEP_FILES),
|
||||||
help=_('''
|
help=_('''
|
||||||
HTTP header (key:value), data field (key=value)
|
HTTP header (header:value), data field (field=value),
|
||||||
or raw JSON field (field:=value).
|
raw JSON field (field:=value)
|
||||||
|
or file field (field@/path/to/file).
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
|
1
tests/file.txt
Normal file
1
tests/file.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
__test_file_content__
|
@ -1,7 +1,13 @@
|
|||||||
|
# coding:utf8
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
import argparse
|
import argparse
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
TESTS_ROOT = os.path.dirname(__file__)
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(TESTS_ROOT, '..')))
|
||||||
from httpie import __main__
|
from httpie import __main__
|
||||||
from httpie import cli
|
from httpie import cli
|
||||||
|
|
||||||
@ -82,7 +88,7 @@ class TestHTTPie(BaseTest):
|
|||||||
self.assertIn('"foo": "bar"', response)
|
self.assertIn('"foo": "bar"', response)
|
||||||
|
|
||||||
def test_form(self):
|
def test_form(self):
|
||||||
response = http('POST', '--form', 'http://httpbin.org/post', 'foo=bar')
|
response = http('--form', 'POST', 'http://httpbin.org/post', 'foo=bar')
|
||||||
self.assertIn('"foo": "bar"', response)
|
self.assertIn('"foo": "bar"', response)
|
||||||
|
|
||||||
def test_headers(self):
|
def test_headers(self):
|
||||||
@ -103,13 +109,27 @@ class TestPrettyFlag(BaseTest):
|
|||||||
self.assertNotIn(TERMINAL_COLOR_CHECK, r)
|
self.assertNotIn(TERMINAL_COLOR_CHECK, r)
|
||||||
|
|
||||||
def test_force_pretty(self):
|
def test_force_pretty(self):
|
||||||
r = http('GET', '--pretty', 'http://httpbin.org/get', stdout_isatty=False)
|
r = http('--pretty', 'GET', 'http://httpbin.org/get', stdout_isatty=False)
|
||||||
self.assertIn(TERMINAL_COLOR_CHECK, r)
|
self.assertIn(TERMINAL_COLOR_CHECK, r)
|
||||||
|
|
||||||
def test_force_ugly(self):
|
def test_force_ugly(self):
|
||||||
r = http('GET', '--ugly', 'http://httpbin.org/get', stdout_isatty=True)
|
r = http('--ugly', 'GET', 'http://httpbin.org/get', stdout_isatty=True)
|
||||||
self.assertNotIn(TERMINAL_COLOR_CHECK, r)
|
self.assertNotIn(TERMINAL_COLOR_CHECK, r)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileUpload(BaseTest):
|
||||||
|
|
||||||
|
def test_non_existent_file_raises_parse_error(self):
|
||||||
|
self.assertRaises(cli.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' % os.path.join(TESTS_ROOT, 'file.txt'))
|
||||||
|
self.assertIn('"test-file": "__test_file_content__', r)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
Loading…
Reference in New Issue
Block a user