mirror of
https://github.com/httpie/cli.git
synced 2025-06-24 19:41:23 +02:00
Added support for request payload from a filepath
Content-Type is detected from the filename. Closes #57.
This commit is contained in:
parent
41d640920c
commit
50196be0f2
@ -106,6 +106,10 @@ The above can be further simplified by omitting ``GET`` and ``POST`` because the
|
|||||||
|
|
||||||
http -b https://api.github.com/repos/jkbr/httpie | http httpbin.org/post
|
http -b https://api.github.com/repos/jkbr/httpie | http httpbin.org/post
|
||||||
|
|
||||||
|
An alternative to ``stdin`` is to pass a file name whose content will be used as the request body. It has the advantage that the ``Content-Type`` header will automatically be set to the appropriate value based on the filename extension (using the ``mimetypes`` module). Therefore, the following will request will send the verbatim contents of the file with ``Content-Type: application/xml``::
|
||||||
|
|
||||||
|
http PUT httpbin.org/put @/data/file.xml
|
||||||
|
|
||||||
|
|
||||||
Flags
|
Flags
|
||||||
^^^^^
|
^^^^^
|
||||||
@ -215,6 +219,7 @@ Changelog
|
|||||||
---------
|
---------
|
||||||
|
|
||||||
* `0.2.3dev <https://github.com/jkbr/httpie/compare/0.2.2...master>`_
|
* `0.2.3dev <https://github.com/jkbr/httpie/compare/0.2.2...master>`_
|
||||||
|
* Added support for request payloads from a file path with automatic ``Content-Type`` (``http URL @/path``).
|
||||||
* `0.2.2 <https://github.com/jkbr/httpie/compare/0.2.1...0.2.2>`_ (2012-06-24)
|
* `0.2.2 <https://github.com/jkbr/httpie/compare/0.2.1...0.2.2>`_ (2012-06-24)
|
||||||
* The ``METHOD`` positional argument can now be omitted (defaults to ``GET``, or to ``POST`` with data).
|
* The ``METHOD`` positional argument can now be omitted (defaults to ``GET``, or to ``POST`` with data).
|
||||||
* Fixed --verbose --form.
|
* Fixed --verbose --form.
|
||||||
|
@ -24,8 +24,8 @@ def _get_response(parser, args, stdin, stdin_isatty):
|
|||||||
'Content-Type' not in args.headers
|
'Content-Type' not in args.headers
|
||||||
and (args.data or args.json)):
|
and (args.data or args.json)):
|
||||||
args.headers['Content-Type'] = TYPE_JSON
|
args.headers['Content-Type'] = TYPE_JSON
|
||||||
if stdin_isatty:
|
if isinstance(args.data, dict):
|
||||||
# Serialize the parsed data.
|
# Serialize the data dict parsed from arguments.
|
||||||
args.data = json.dumps(args.data)
|
args.data = json.dumps(args.data)
|
||||||
if 'Accept' not in args.headers:
|
if 'Accept' not in args.headers:
|
||||||
# Default Accept to JSON as well.
|
# Default Accept to JSON as well.
|
||||||
|
@ -25,16 +25,16 @@ 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=_('''
|
||||||
(default) Data items are serialized as a JSON object.
|
(default) Data items from the command line are serialized as a JSON object.
|
||||||
The Content-Type and Accept headers
|
The Content-Type and Accept headers
|
||||||
are set to application/json (if not set via the command line).
|
are set to application/json (if not specified).
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
group_type.add_argument(
|
group_type.add_argument(
|
||||||
'--form', '-f', action='store_true',
|
'--form', '-f', action='store_true',
|
||||||
help=_('''
|
help=_('''
|
||||||
Data items are serialized as form fields.
|
Data items from the command line are serialized as form fields.
|
||||||
The Content-Type is set to application/x-www-form-urlencoded (if not specifid).
|
The Content-Type is set to application/x-www-form-urlencoded (if not specified).
|
||||||
The presence of any file fields results into a multipart/form-data request.
|
The presence of any file fields results into a multipart/form-data request.
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
|
@ -7,6 +7,7 @@ import sys
|
|||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
@ -58,14 +59,14 @@ class Parser(argparse.ArgumentParser):
|
|||||||
self._guess_method(args, stdin_isatty)
|
self._guess_method(args, stdin_isatty)
|
||||||
self._parse_items(args)
|
self._parse_items(args)
|
||||||
if not stdin_isatty:
|
if not stdin_isatty:
|
||||||
self._process_stdin(args, stdin)
|
self._body_from_file(args, stdin)
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def _process_stdin(self, args, stdin):
|
def _body_from_file(self, args, f):
|
||||||
if args.data:
|
if args.data:
|
||||||
self.error('Request body (stdin) and request '
|
self.error('Request body (from stdin or a file) and request '
|
||||||
'data (key=value) cannot be mixed.')
|
'data (key=value) cannot be mixed.')
|
||||||
args.data = stdin.read()
|
args.data = f.read()
|
||||||
|
|
||||||
def _guess_method(self, args, stdin_isatty=sys.stdin.isatty()):
|
def _guess_method(self, args, stdin_isatty=sys.stdin.isatty()):
|
||||||
"""
|
"""
|
||||||
@ -125,12 +126,26 @@ class Parser(argparse.ArgumentParser):
|
|||||||
self.error(e.message)
|
self.error(e.message)
|
||||||
|
|
||||||
if args.files and not args.form:
|
if args.files and not args.form:
|
||||||
# We could just switch to --form automatically here,
|
# `http url @/path/to/file`
|
||||||
# but I think it's better to make it explicit.
|
# It's not --form so the file contents will be used as the
|
||||||
self.error(
|
# body of the requests. Also, we try to detect the appropriate
|
||||||
' You need to set the --form / -f flag to'
|
# Content-Type.
|
||||||
' to issue a multipart request. File fields: %s'
|
if len(args.files) > 1:
|
||||||
% ','.join(args.files.keys()))
|
self.error(
|
||||||
|
'Only one file can be specified unless'
|
||||||
|
' --form is used. File fields: %s'
|
||||||
|
% ','.join(args.files.keys()))
|
||||||
|
f = list(args.files.values())[0]
|
||||||
|
self._body_from_file(args, f)
|
||||||
|
args.files = {}
|
||||||
|
if 'Content-Type' not in args.headers:
|
||||||
|
mime, encoding = mimetypes.guess_type(f.name, strict=False)
|
||||||
|
if mime:
|
||||||
|
content_type = mime
|
||||||
|
if encoding:
|
||||||
|
content_type = '%s; charset=%s' % (mime, encoding)
|
||||||
|
args.headers['Content-Type'] = content_type
|
||||||
|
|
||||||
|
|
||||||
def _validate_output_options(self, args):
|
def _validate_output_options(self, args):
|
||||||
unknown_output_options = set(args.output_options) - set(OUTPUT_OPTIONS)
|
unknown_output_options = set(args.output_options) - set(OUTPUT_OPTIONS)
|
||||||
|
1
tests/file2.txt
Normal file
1
tests/file2.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
__test_file_content__
|
@ -18,6 +18,7 @@ from httpie import __main__, cliparse
|
|||||||
|
|
||||||
|
|
||||||
TEST_FILE_PATH = os.path.join(TESTS_ROOT, 'file.txt')
|
TEST_FILE_PATH = os.path.join(TESTS_ROOT, 'file.txt')
|
||||||
|
TEST_FILE2_PATH = os.path.join(TESTS_ROOT, 'file2.txt')
|
||||||
TEST_FILE_CONTENT = open(TEST_FILE_PATH).read().strip()
|
TEST_FILE_CONTENT = open(TEST_FILE_PATH).read().strip()
|
||||||
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
|
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
|
||||||
|
|
||||||
@ -180,10 +181,42 @@ class MultipartFormDataFileUploadTest(BaseTestCase):
|
|||||||
r = http('--form', 'POST', 'http://httpbin.org/post',
|
r = http('--form', 'POST', 'http://httpbin.org/post',
|
||||||
'test-file@%s' % TEST_FILE_PATH, 'foo=bar')
|
'test-file@%s' % TEST_FILE_PATH, 'foo=bar')
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"test-file": "__test_file_content__', r)
|
self.assertIn('"test-file": "%s' % TEST_FILE_CONTENT, r)
|
||||||
self.assertIn('"foo": "bar"', r)
|
self.assertIn('"foo": "bar"', r)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestBodyFromFilePathTest(BaseTestCase):
|
||||||
|
"""
|
||||||
|
`http URL @file'
|
||||||
|
|
||||||
|
"""
|
||||||
|
def test_request_body_from_file_by_path(self):
|
||||||
|
r = http('POST', 'http://httpbin.org/post', '@' + TEST_FILE_PATH)
|
||||||
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
|
self.assertIn(TEST_FILE_CONTENT, r)
|
||||||
|
self.assertIn('"Content-Type": "text/plain"', r)
|
||||||
|
|
||||||
|
def test_request_body_from_file_by_path_with_explicit_content_type(self):
|
||||||
|
r = http('POST', 'http://httpbin.org/post', '@' + TEST_FILE_PATH, 'Content-Type:x-foo/bar')
|
||||||
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
|
self.assertIn(TEST_FILE_CONTENT, r)
|
||||||
|
self.assertIn('"Content-Type": "x-foo/bar"', r)
|
||||||
|
|
||||||
|
def test_request_body_from_file_by_path_only_one_file_allowed(self):
|
||||||
|
self.assertRaises(SystemExit, lambda: http(
|
||||||
|
'POST',
|
||||||
|
'http://httpbin.org/post',
|
||||||
|
'@' + TEST_FILE_PATH,
|
||||||
|
'@' + TEST_FILE2_PATH))
|
||||||
|
|
||||||
|
def test_request_body_from_file_by_path_only_no_data_items_allowed(self):
|
||||||
|
self.assertRaises(SystemExit, lambda: http(
|
||||||
|
'POST',
|
||||||
|
'http://httpbin.org/post',
|
||||||
|
'@' + TEST_FILE_PATH,
|
||||||
|
'foo=bar'))
|
||||||
|
|
||||||
|
|
||||||
class AuthTest(BaseTestCase):
|
class AuthTest(BaseTestCase):
|
||||||
|
|
||||||
def test_basic_auth(self):
|
def test_basic_auth(self):
|
||||||
@ -273,7 +306,7 @@ class ItemParsingTest(BaseTestCase):
|
|||||||
self.assertIn('test-file', files)
|
self.assertIn('test-file', files)
|
||||||
|
|
||||||
|
|
||||||
class HTTPieArgumentParserTestCase(unittest.TestCase):
|
class ArgumentParserTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.parser = cliparse.Parser()
|
self.parser = cliparse.Parser()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user