Added support for request payload from a filepath

Content-Type is detected from the filename.

Closes #57.
This commit is contained in:
Jakub Roztocil 2012-06-29 00:45:31 +02:00
parent 41d640920c
commit 50196be0f2
6 changed files with 72 additions and 18 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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.
''')
)

View File

@ -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
View File

@ -0,0 +1 @@
__test_file_content__

View File

@ -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()