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

View File

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

View File

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

View File

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