mirror of
https://github.com/httpie/cli.git
synced 2024-12-23 23:09:17 +01: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
|
||||
|
||||
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
|
||||
^^^^^
|
||||
@ -215,6 +219,7 @@ Changelog
|
||||
---------
|
||||
|
||||
* `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)
|
||||
* The ``METHOD`` positional argument can now be omitted (defaults to ``GET``, or to ``POST`` with data).
|
||||
* Fixed --verbose --form.
|
||||
|
@ -24,8 +24,8 @@ def _get_response(parser, args, stdin, stdin_isatty):
|
||||
'Content-Type' not in args.headers
|
||||
and (args.data or args.json)):
|
||||
args.headers['Content-Type'] = TYPE_JSON
|
||||
if stdin_isatty:
|
||||
# Serialize the parsed data.
|
||||
if isinstance(args.data, dict):
|
||||
# Serialize the data dict parsed from arguments.
|
||||
args.data = json.dumps(args.data)
|
||||
if 'Accept' not in args.headers:
|
||||
# Default Accept to JSON as well.
|
||||
|
@ -25,16 +25,16 @@ group_type = parser.add_mutually_exclusive_group(required=False)
|
||||
group_type.add_argument(
|
||||
'--json', '-j', action='store_true',
|
||||
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
|
||||
are set to application/json (if not set via the command line).
|
||||
are set to application/json (if not specified).
|
||||
''')
|
||||
)
|
||||
group_type.add_argument(
|
||||
'--form', '-f', action='store_true',
|
||||
help=_('''
|
||||
Data items are serialized as form fields.
|
||||
The Content-Type is set to application/x-www-form-urlencoded (if not specifid).
|
||||
Data items from the command line are serialized as form fields.
|
||||
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.
|
||||
''')
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ import sys
|
||||
import re
|
||||
import json
|
||||
import argparse
|
||||
import mimetypes
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
@ -58,14 +59,14 @@ class Parser(argparse.ArgumentParser):
|
||||
self._guess_method(args, stdin_isatty)
|
||||
self._parse_items(args)
|
||||
if not stdin_isatty:
|
||||
self._process_stdin(args, stdin)
|
||||
self._body_from_file(args, stdin)
|
||||
return args
|
||||
|
||||
def _process_stdin(self, args, stdin):
|
||||
def _body_from_file(self, args, f):
|
||||
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.')
|
||||
args.data = stdin.read()
|
||||
args.data = f.read()
|
||||
|
||||
def _guess_method(self, args, stdin_isatty=sys.stdin.isatty()):
|
||||
"""
|
||||
@ -125,12 +126,26 @@ class Parser(argparse.ArgumentParser):
|
||||
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()))
|
||||
# `http url @/path/to/file`
|
||||
# It's not --form so the file contents will be used as the
|
||||
# body of the requests. Also, we try to detect the appropriate
|
||||
# Content-Type.
|
||||
if len(args.files) > 1:
|
||||
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):
|
||||
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_FILE2_PATH = os.path.join(TESTS_ROOT, 'file2.txt')
|
||||
TEST_FILE_CONTENT = open(TEST_FILE_PATH).read().strip()
|
||||
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
|
||||
|
||||
@ -180,10 +181,42 @@ class MultipartFormDataFileUploadTest(BaseTestCase):
|
||||
r = http('--form', 'POST', 'http://httpbin.org/post',
|
||||
'test-file@%s' % TEST_FILE_PATH, 'foo=bar')
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
|
||||
def test_basic_auth(self):
|
||||
@ -273,7 +306,7 @@ class ItemParsingTest(BaseTestCase):
|
||||
self.assertIn('test-file', files)
|
||||
|
||||
|
||||
class HTTPieArgumentParserTestCase(unittest.TestCase):
|
||||
class ArgumentParserTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.parser = cliparse.Parser()
|
||||
|
Loading…
Reference in New Issue
Block a user