mirror of
https://github.com/httpie/cli.git
synced 2024-11-26 01:33:33 +01:00
Added exit status constants, cleaned up main().
This commit is contained in:
parent
94c77c9bfc
commit
4e58a3849a
@ -5,3 +5,12 @@ HTTPie - cURL for humans.
|
|||||||
__author__ = 'Jakub Roztocil'
|
__author__ = 'Jakub Roztocil'
|
||||||
__version__ = '0.2.7dev'
|
__version__ = '0.2.7dev'
|
||||||
__licence__ = 'BSD'
|
__licence__ = 'BSD'
|
||||||
|
|
||||||
|
|
||||||
|
class EXIT:
|
||||||
|
OK = 0
|
||||||
|
ERROR = 1
|
||||||
|
# Used only when requested:
|
||||||
|
ERROR_HTTP_3XX = 3
|
||||||
|
ERROR_HTTP_4XX = 4
|
||||||
|
ERROR_HTTP_5XX = 5
|
||||||
|
124
httpie/core.py
124
httpie/core.py
@ -13,27 +13,22 @@ Invocation flow:
|
|||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import errno
|
import errno
|
||||||
from itertools import chain
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import requests.auth
|
import requests.auth
|
||||||
from requests.compat import str
|
from requests.compat import str
|
||||||
|
|
||||||
from .models import HTTPRequest, HTTPResponse, Environment
|
|
||||||
from .output import (OutputProcessor, RawStream, PrettyStream,
|
|
||||||
BufferedPrettyStream, EncodedStream)
|
|
||||||
|
|
||||||
from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
|
|
||||||
OUT_RESP_HEAD, OUT_RESP_BODY)
|
|
||||||
from .cli import parser
|
from .cli import parser
|
||||||
|
from .models import Environment
|
||||||
|
from .output import output_stream, write
|
||||||
|
from . import EXIT
|
||||||
|
|
||||||
|
|
||||||
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||||
JSON = 'application/json; charset=utf-8'
|
JSON = 'application/json; charset=utf-8'
|
||||||
|
|
||||||
|
|
||||||
def get_response(args, env):
|
def get_response(args):
|
||||||
"""Send the request and return a `request.Response`."""
|
"""Send the request and return a `request.Response`."""
|
||||||
|
|
||||||
auto_json = args.data and not args.form
|
auto_json = args.data and not args.form
|
||||||
@ -78,71 +73,19 @@ def get_response(args, env):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def output_stream(args, env, request, response):
|
|
||||||
"""Format parts of the `request`-`response` exchange
|
|
||||||
according to `args` and `env` and return `bytes`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Pick the right stream type for this exchange based on `env` and `args`.
|
|
||||||
if not env.stdout_isatty and not args.prettify:
|
|
||||||
Stream = partial(
|
|
||||||
RawStream,
|
|
||||||
chunk_size=RawStream.CHUNK_SIZE_BY_LINE
|
|
||||||
if args.stream
|
|
||||||
else RawStream.CHUNK_SIZE)
|
|
||||||
elif args.prettify:
|
|
||||||
Stream = partial(
|
|
||||||
PrettyStream if args.stream else BufferedPrettyStream,
|
|
||||||
processor=OutputProcessor(env, pygments_style=args.style),
|
|
||||||
env=env)
|
|
||||||
else:
|
|
||||||
Stream = partial(EncodedStream, env=env)
|
|
||||||
|
|
||||||
req_h = OUT_REQ_HEAD in args.output_options
|
|
||||||
req_b = OUT_REQ_BODY in args.output_options
|
|
||||||
resp_h = OUT_RESP_HEAD in args.output_options
|
|
||||||
resp_b = OUT_RESP_BODY in args.output_options
|
|
||||||
|
|
||||||
req = req_h or req_b
|
|
||||||
resp = resp_h or resp_b
|
|
||||||
|
|
||||||
output = []
|
|
||||||
|
|
||||||
if req:
|
|
||||||
output.append(Stream(
|
|
||||||
msg=HTTPRequest(request),
|
|
||||||
with_headers=req_h,
|
|
||||||
with_body=req_b))
|
|
||||||
|
|
||||||
if req and resp:
|
|
||||||
output.append([b'\n\n\n'])
|
|
||||||
|
|
||||||
if resp:
|
|
||||||
output.append(Stream(
|
|
||||||
msg=HTTPResponse(response),
|
|
||||||
with_headers=resp_h,
|
|
||||||
with_body=resp_b))
|
|
||||||
|
|
||||||
if env.stdout_isatty:
|
|
||||||
output.append([b'\n\n'])
|
|
||||||
|
|
||||||
return chain(*output)
|
|
||||||
|
|
||||||
|
|
||||||
def get_exist_status(code, allow_redirects=False):
|
def get_exist_status(code, allow_redirects=False):
|
||||||
"""Translate HTTP status code to exit status."""
|
"""Translate HTTP status code to exit status."""
|
||||||
if 300 <= code <= 399 and not allow_redirects:
|
if 300 <= code <= 399 and not allow_redirects:
|
||||||
# Redirect
|
# Redirect
|
||||||
return 3
|
return EXIT.ERROR_HTTP_3XX
|
||||||
elif 400 <= code <= 499:
|
elif 400 <= code <= 499:
|
||||||
# Client Error
|
# Client Error
|
||||||
return 4
|
return EXIT.ERROR_HTTP_4XX
|
||||||
elif 500 <= code <= 599:
|
elif 500 <= code <= 599:
|
||||||
# Server Error
|
# Server Error
|
||||||
return 5
|
return EXIT.ERROR_HTTP_5XX
|
||||||
else:
|
else:
|
||||||
return 0
|
return EXIT.OK
|
||||||
|
|
||||||
|
|
||||||
def main(args=sys.argv[1:], env=Environment()):
|
def main(args=sys.argv[1:], env=Environment()):
|
||||||
@ -151,57 +94,50 @@ def main(args=sys.argv[1:], env=Environment()):
|
|||||||
Return exit status.
|
Return exit status.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
debug = '--debug' in args
|
|
||||||
|
|
||||||
if env.is_windows and not env.stdout_isatty:
|
def error(msg, *args):
|
||||||
env.stderr.write(
|
msg = msg % args
|
||||||
'http: error: Output redirection is not supported on Windows.'
|
env.stderr.write('http: error: %s\n' % msg)
|
||||||
' Please use `--output FILE\' instead.\n')
|
|
||||||
return 1
|
debug = '--debug' in args
|
||||||
|
status = EXIT.OK
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args = parser.parse_args(args=args, env=env)
|
args = parser.parse_args(args=args, env=env)
|
||||||
response = get_response(args, env)
|
response = get_response(args)
|
||||||
status = 0
|
|
||||||
|
|
||||||
if args.check_status:
|
if args.check_status:
|
||||||
status = get_exist_status(response.status_code,
|
status = get_exist_status(response.status_code,
|
||||||
args.allow_redirects)
|
args.allow_redirects)
|
||||||
if status and not env.stdout_isatty:
|
if status and not env.stdout_isatty:
|
||||||
err = 'http error: %s %s\n' % (
|
error('%s %s', response.raw.status, response.raw.reason)
|
||||||
response.raw.status, response.raw.reason)
|
|
||||||
env.stderr.write(err)
|
stream = output_stream(args, env, response.request, response)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# We are writing bytes so we use buffer on Python 3
|
write(stream=stream,
|
||||||
buffer = env.stdout.buffer
|
outfile=env.stdout,
|
||||||
except AttributeError:
|
flush=env.stdout_isatty or args.stream)
|
||||||
buffer = env.stdout
|
|
||||||
|
|
||||||
try:
|
|
||||||
for chunk in output_stream(args, env, response.request, response):
|
|
||||||
buffer.write(chunk)
|
|
||||||
if env.stdout_isatty or args.stream:
|
|
||||||
env.stdout.flush()
|
|
||||||
|
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
if debug:
|
if not debug and e.errno == errno.EPIPE:
|
||||||
raise
|
# Ignore broken pipes unless --debug.
|
||||||
if e.errno == errno.EPIPE:
|
|
||||||
env.stderr.write('\n')
|
env.stderr.write('\n')
|
||||||
else:
|
else:
|
||||||
env.stderr.write(str(e) + '\n')
|
raise
|
||||||
return 1
|
|
||||||
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
if debug:
|
if debug:
|
||||||
raise
|
raise
|
||||||
env.stderr.write('\n')
|
env.stderr.write('\n')
|
||||||
return 1
|
status = EXIT.ERROR
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# TODO: distinguish between expected and unexpected errors.
|
||||||
|
# network errors vs. bugs, etc.
|
||||||
if debug:
|
if debug:
|
||||||
raise
|
raise
|
||||||
env.stderr.write(str(e) + '\n')
|
error('%s: %s', type(e).__name__, str(e))
|
||||||
return 1
|
status = EXIT.ERROR
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
@ -95,6 +95,10 @@ class Parser(argparse.ArgumentParser):
|
|||||||
|
|
||||||
self.env = env
|
self.env = env
|
||||||
|
|
||||||
|
if env.is_windows and not env.stdout_isatty:
|
||||||
|
self.error('Output redirection is not supported on Windows.'
|
||||||
|
' Please use `--output FILE\' instead.')
|
||||||
|
|
||||||
args = super(Parser, self).parse_args(args, namespace)
|
args = super(Parser, self).parse_args(args, namespace)
|
||||||
|
|
||||||
if args.output:
|
if args.output:
|
||||||
@ -131,9 +135,6 @@ class Parser(argparse.ArgumentParser):
|
|||||||
None: self.env.stderr
|
None: self.env.stderr
|
||||||
}.get(file, file)
|
}.get(file, file)
|
||||||
|
|
||||||
#if isinstance(message, str):
|
|
||||||
# message = message.encode('utf8')
|
|
||||||
|
|
||||||
super(Parser, self)._print_message(message, file)
|
super(Parser, self)._print_message(message, file)
|
||||||
|
|
||||||
def _body_from_file(self, args, fd):
|
def _body_from_file(self, args, fd):
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
from functools import partial
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
import pygments
|
import pygments
|
||||||
from pygments import token, lexer
|
from pygments import token, lexer
|
||||||
@ -13,7 +15,9 @@ from pygments.util import ClassNotFound
|
|||||||
from requests.compat import is_windows
|
from requests.compat import is_windows
|
||||||
|
|
||||||
from .solarized import Solarized256Style
|
from .solarized import Solarized256Style
|
||||||
from .models import Environment
|
from .models import HTTPRequest, HTTPResponse, Environment
|
||||||
|
from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||||
|
OUT_RESP_HEAD, OUT_RESP_BODY)
|
||||||
|
|
||||||
|
|
||||||
# Colors on Windows via colorama aren't that great and fruity
|
# Colors on Windows via colorama aren't that great and fruity
|
||||||
@ -43,6 +47,82 @@ class BinarySuppressedError(Exception):
|
|||||||
# Output Streams
|
# Output Streams
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
|
def write(stream, outfile, flush):
|
||||||
|
"""Write the output stream."""
|
||||||
|
try:
|
||||||
|
# Writing bytes so we use the buffer interface (Python 3).
|
||||||
|
buf = outfile.buffer
|
||||||
|
except AttributeError:
|
||||||
|
buf = outfile
|
||||||
|
|
||||||
|
for chunk in stream:
|
||||||
|
buf.write(chunk)
|
||||||
|
if flush:
|
||||||
|
outfile.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def output_stream(args, env, request, response):
|
||||||
|
"""Build and return a chain of iterators over the `request`-`response`
|
||||||
|
exchange each of which yields `bytes` chunks.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
Stream = make_stream(env, args)
|
||||||
|
|
||||||
|
req_h = OUT_REQ_HEAD in args.output_options
|
||||||
|
req_b = OUT_REQ_BODY in args.output_options
|
||||||
|
resp_h = OUT_RESP_HEAD in args.output_options
|
||||||
|
resp_b = OUT_RESP_BODY in args.output_options
|
||||||
|
|
||||||
|
req = req_h or req_b
|
||||||
|
resp = resp_h or resp_b
|
||||||
|
|
||||||
|
output = []
|
||||||
|
|
||||||
|
if req:
|
||||||
|
output.append(Stream(
|
||||||
|
msg=HTTPRequest(request),
|
||||||
|
with_headers=req_h,
|
||||||
|
with_body=req_b))
|
||||||
|
|
||||||
|
if req and resp:
|
||||||
|
output.append([b'\n\n\n'])
|
||||||
|
|
||||||
|
if resp:
|
||||||
|
output.append(Stream(
|
||||||
|
msg=HTTPResponse(response),
|
||||||
|
with_headers=resp_h,
|
||||||
|
with_body=resp_b))
|
||||||
|
|
||||||
|
if env.stdout_isatty:
|
||||||
|
output.append([b'\n\n'])
|
||||||
|
|
||||||
|
return chain(*output)
|
||||||
|
|
||||||
|
|
||||||
|
def make_stream(env, args):
|
||||||
|
"""Pick the right stream type for based on `env` and `args`.
|
||||||
|
and wrap it with a partial with the type-specific args.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not env.stdout_isatty and not args.prettify:
|
||||||
|
Stream = partial(
|
||||||
|
RawStream,
|
||||||
|
chunk_size=RawStream.CHUNK_SIZE_BY_LINE
|
||||||
|
if args.stream
|
||||||
|
else RawStream.CHUNK_SIZE)
|
||||||
|
elif args.prettify:
|
||||||
|
Stream = partial(
|
||||||
|
PrettyStream if args.stream else BufferedPrettyStream,
|
||||||
|
processor=OutputProcessor(env, pygments_style=args.style),
|
||||||
|
env=env)
|
||||||
|
else:
|
||||||
|
Stream = partial(EncodedStream, env=env)
|
||||||
|
|
||||||
|
return Stream
|
||||||
|
|
||||||
|
|
||||||
class BaseStream(object):
|
class BaseStream(object):
|
||||||
"""Base HTTP message stream class."""
|
"""Base HTTP message stream class."""
|
||||||
|
|
||||||
|
@ -25,21 +25,26 @@ import json
|
|||||||
import argparse
|
import argparse
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
try:
|
|
||||||
from unittest import skipIf
|
|
||||||
except ImportError:
|
|
||||||
def skipIf(cond, test_method):
|
|
||||||
if cond:
|
|
||||||
return test_method
|
|
||||||
return lambda self: None
|
|
||||||
try:
|
try:
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib2 import urlopen
|
from urllib2 import urlopen
|
||||||
|
try:
|
||||||
|
from unittest import skipIf
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def skipIf(cond, reason):
|
||||||
|
def decorator(test_method):
|
||||||
|
if cond:
|
||||||
|
return lambda self: None
|
||||||
|
return test_method
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
from requests.compat import is_windows, is_py26, bytes, str
|
from requests.compat import is_windows, is_py26, bytes, str
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
#################################################################
|
||||||
# Utils/setup
|
# Utils/setup
|
||||||
#################################################################
|
#################################################################
|
||||||
@ -48,6 +53,7 @@ from requests.compat import is_windows, is_py26, bytes, str
|
|||||||
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||||
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
|
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
|
||||||
|
|
||||||
|
from httpie import EXIT
|
||||||
from httpie import input
|
from httpie import input
|
||||||
from httpie.models import Environment
|
from httpie.models import Environment
|
||||||
from httpie.core import main
|
from httpie.core import main
|
||||||
@ -108,8 +114,10 @@ class TestEnvironment(Environment):
|
|||||||
super(TestEnvironment, self).__init__(**kwargs)
|
super(TestEnvironment, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class BytesResponse(bytes): pass
|
class BytesResponse(bytes):
|
||||||
class StrResponse(str): pass
|
stderr = json = exit_status = None
|
||||||
|
class StrResponse(str):
|
||||||
|
stderr = json = exit_status = None
|
||||||
|
|
||||||
|
|
||||||
def http(*args, **kwargs):
|
def http(*args, **kwargs):
|
||||||
@ -139,7 +147,7 @@ def http(*args, **kwargs):
|
|||||||
sys.stderr.write(env.stderr.read())
|
sys.stderr.write(env.stderr.read())
|
||||||
raise
|
raise
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
exit_status = 1
|
exit_status = EXIT.ERROR
|
||||||
|
|
||||||
env.stdout.seek(0)
|
env.stdout.seek(0)
|
||||||
env.stderr.seek(0)
|
env.stderr.seek(0)
|
||||||
@ -792,7 +800,7 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
httpbin('/status/200')
|
httpbin('/status/200')
|
||||||
)
|
)
|
||||||
self.assertIn(OK, r)
|
self.assertIn(OK, r)
|
||||||
self.assertEqual(r.exit_status, 0)
|
self.assertEqual(r.exit_status, EXIT.OK)
|
||||||
|
|
||||||
def test_error_response_exits_0_without_check_status(self):
|
def test_error_response_exits_0_without_check_status(self):
|
||||||
r = http(
|
r = http(
|
||||||
@ -800,7 +808,7 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
httpbin('/status/500')
|
httpbin('/status/500')
|
||||||
)
|
)
|
||||||
self.assertIn('HTTP/1.1 500', r)
|
self.assertIn('HTTP/1.1 500', r)
|
||||||
self.assertEqual(r.exit_status, 0)
|
self.assertEqual(r.exit_status, EXIT.OK)
|
||||||
|
|
||||||
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
|
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
|
||||||
r = http(
|
r = http(
|
||||||
@ -811,7 +819,7 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
env=TestEnvironment(stdout_isatty=False,)
|
env=TestEnvironment(stdout_isatty=False,)
|
||||||
)
|
)
|
||||||
self.assertIn('HTTP/1.1 301', r)
|
self.assertIn('HTTP/1.1 301', r)
|
||||||
self.assertEqual(r.exit_status, 3)
|
self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_3XX)
|
||||||
self.assertIn('301 moved permanently', r.stderr.lower())
|
self.assertIn('301 moved permanently', r.stderr.lower())
|
||||||
|
|
||||||
def test_3xx_check_status_redirects_allowed_exits_0(self):
|
def test_3xx_check_status_redirects_allowed_exits_0(self):
|
||||||
@ -823,7 +831,7 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
)
|
)
|
||||||
# The redirect will be followed so 200 is expected.
|
# The redirect will be followed so 200 is expected.
|
||||||
self.assertIn('HTTP/1.1 200 OK', r)
|
self.assertIn('HTTP/1.1 200 OK', r)
|
||||||
self.assertEqual(r.exit_status, 0)
|
self.assertEqual(r.exit_status, EXIT.OK)
|
||||||
|
|
||||||
def test_4xx_check_status_exits_4(self):
|
def test_4xx_check_status_exits_4(self):
|
||||||
r = http(
|
r = http(
|
||||||
@ -832,7 +840,7 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
httpbin('/status/401')
|
httpbin('/status/401')
|
||||||
)
|
)
|
||||||
self.assertIn('HTTP/1.1 401', r)
|
self.assertIn('HTTP/1.1 401', r)
|
||||||
self.assertEqual(r.exit_status, 4)
|
self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_4XX)
|
||||||
# Also stderr should be empty since stdout isn't redirected.
|
# Also stderr should be empty since stdout isn't redirected.
|
||||||
self.assertTrue(not r.stderr)
|
self.assertTrue(not r.stderr)
|
||||||
|
|
||||||
@ -843,7 +851,7 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
httpbin('/status/500')
|
httpbin('/status/500')
|
||||||
)
|
)
|
||||||
self.assertIn('HTTP/1.1 500', r)
|
self.assertIn('HTTP/1.1 500', r)
|
||||||
self.assertEqual(r.exit_status, 5)
|
self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_5XX)
|
||||||
|
|
||||||
|
|
||||||
class FakeWindowsTest(BaseTestCase):
|
class FakeWindowsTest(BaseTestCase):
|
||||||
@ -855,7 +863,7 @@ class FakeWindowsTest(BaseTestCase):
|
|||||||
httpbin('/get'),
|
httpbin('/get'),
|
||||||
env=env
|
env=env
|
||||||
)
|
)
|
||||||
self.assertNotEqual(r.exit_status, 0)
|
self.assertNotEqual(r.exit_status, EXIT.OK)
|
||||||
self.assertIn('Windows', r.stderr)
|
self.assertIn('Windows', r.stderr)
|
||||||
self.assertIn('--output', r.stderr)
|
self.assertIn('--output', r.stderr)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user