mirror of
https://github.com/httpie/cli.git
synced 2024-11-28 08:38:44 +02: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'
|
||||
__version__ = '0.2.7dev'
|
||||
__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 json
|
||||
import errno
|
||||
from itertools import chain
|
||||
from functools import partial
|
||||
|
||||
import requests
|
||||
import requests.auth
|
||||
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 .models import Environment
|
||||
from .output import output_stream, write
|
||||
from . import EXIT
|
||||
|
||||
|
||||
FORM = 'application/x-www-form-urlencoded; 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`."""
|
||||
|
||||
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):
|
||||
"""Translate HTTP status code to exit status."""
|
||||
if 300 <= code <= 399 and not allow_redirects:
|
||||
# Redirect
|
||||
return 3
|
||||
return EXIT.ERROR_HTTP_3XX
|
||||
elif 400 <= code <= 499:
|
||||
# Client Error
|
||||
return 4
|
||||
return EXIT.ERROR_HTTP_4XX
|
||||
elif 500 <= code <= 599:
|
||||
# Server Error
|
||||
return 5
|
||||
return EXIT.ERROR_HTTP_5XX
|
||||
else:
|
||||
return 0
|
||||
return EXIT.OK
|
||||
|
||||
|
||||
def main(args=sys.argv[1:], env=Environment()):
|
||||
@ -151,57 +94,50 @@ def main(args=sys.argv[1:], env=Environment()):
|
||||
Return exit status.
|
||||
|
||||
"""
|
||||
debug = '--debug' in args
|
||||
|
||||
if env.is_windows and not env.stdout_isatty:
|
||||
env.stderr.write(
|
||||
'http: error: Output redirection is not supported on Windows.'
|
||||
' Please use `--output FILE\' instead.\n')
|
||||
return 1
|
||||
def error(msg, *args):
|
||||
msg = msg % args
|
||||
env.stderr.write('http: error: %s\n' % msg)
|
||||
|
||||
debug = '--debug' in args
|
||||
status = EXIT.OK
|
||||
|
||||
try:
|
||||
args = parser.parse_args(args=args, env=env)
|
||||
response = get_response(args, env)
|
||||
status = 0
|
||||
response = get_response(args)
|
||||
|
||||
if args.check_status:
|
||||
status = get_exist_status(response.status_code,
|
||||
args.allow_redirects)
|
||||
if status and not env.stdout_isatty:
|
||||
err = 'http error: %s %s\n' % (
|
||||
response.raw.status, response.raw.reason)
|
||||
env.stderr.write(err)
|
||||
error('%s %s', response.raw.status, response.raw.reason)
|
||||
|
||||
stream = output_stream(args, env, response.request, response)
|
||||
|
||||
try:
|
||||
# We are writing bytes so we use buffer on Python 3
|
||||
buffer = env.stdout.buffer
|
||||
except AttributeError:
|
||||
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()
|
||||
write(stream=stream,
|
||||
outfile=env.stdout,
|
||||
flush=env.stdout_isatty or args.stream)
|
||||
|
||||
except IOError as e:
|
||||
if debug:
|
||||
raise
|
||||
if e.errno == errno.EPIPE:
|
||||
if not debug and e.errno == errno.EPIPE:
|
||||
# Ignore broken pipes unless --debug.
|
||||
env.stderr.write('\n')
|
||||
else:
|
||||
env.stderr.write(str(e) + '\n')
|
||||
return 1
|
||||
raise
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
if debug:
|
||||
raise
|
||||
env.stderr.write('\n')
|
||||
return 1
|
||||
status = EXIT.ERROR
|
||||
|
||||
except Exception as e:
|
||||
# TODO: distinguish between expected and unexpected errors.
|
||||
# network errors vs. bugs, etc.
|
||||
if debug:
|
||||
raise
|
||||
env.stderr.write(str(e) + '\n')
|
||||
return 1
|
||||
error('%s: %s', type(e).__name__, str(e))
|
||||
status = EXIT.ERROR
|
||||
|
||||
return status
|
||||
|
@ -95,6 +95,10 @@ class Parser(argparse.ArgumentParser):
|
||||
|
||||
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)
|
||||
|
||||
if args.output:
|
||||
@ -131,9 +135,6 @@ class Parser(argparse.ArgumentParser):
|
||||
None: self.env.stderr
|
||||
}.get(file, file)
|
||||
|
||||
#if isinstance(message, str):
|
||||
# message = message.encode('utf8')
|
||||
|
||||
super(Parser, self)._print_message(message, file)
|
||||
|
||||
def _body_from_file(self, args, fd):
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
"""
|
||||
import json
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
|
||||
import pygments
|
||||
from pygments import token, lexer
|
||||
@ -13,7 +15,9 @@ from pygments.util import ClassNotFound
|
||||
from requests.compat import is_windows
|
||||
|
||||
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
|
||||
@ -43,6 +47,82 @@ class BinarySuppressedError(Exception):
|
||||
# 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):
|
||||
"""Base HTTP message stream class."""
|
||||
|
||||
|
@ -25,21 +25,26 @@ import json
|
||||
import argparse
|
||||
import tempfile
|
||||
import unittest
|
||||
try:
|
||||
from unittest import skipIf
|
||||
except ImportError:
|
||||
def skipIf(cond, test_method):
|
||||
if cond:
|
||||
return test_method
|
||||
return lambda self: None
|
||||
try:
|
||||
from urllib.request import urlopen
|
||||
except ImportError:
|
||||
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
|
||||
|
||||
|
||||
|
||||
#################################################################
|
||||
# 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__))
|
||||
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
|
||||
|
||||
from httpie import EXIT
|
||||
from httpie import input
|
||||
from httpie.models import Environment
|
||||
from httpie.core import main
|
||||
@ -108,8 +114,10 @@ class TestEnvironment(Environment):
|
||||
super(TestEnvironment, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class BytesResponse(bytes): pass
|
||||
class StrResponse(str): pass
|
||||
class BytesResponse(bytes):
|
||||
stderr = json = exit_status = None
|
||||
class StrResponse(str):
|
||||
stderr = json = exit_status = None
|
||||
|
||||
|
||||
def http(*args, **kwargs):
|
||||
@ -139,7 +147,7 @@ def http(*args, **kwargs):
|
||||
sys.stderr.write(env.stderr.read())
|
||||
raise
|
||||
except SystemExit:
|
||||
exit_status = 1
|
||||
exit_status = EXIT.ERROR
|
||||
|
||||
env.stdout.seek(0)
|
||||
env.stderr.seek(0)
|
||||
@ -792,7 +800,7 @@ class ExitStatusTest(BaseTestCase):
|
||||
httpbin('/status/200')
|
||||
)
|
||||
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):
|
||||
r = http(
|
||||
@ -800,7 +808,7 @@ class ExitStatusTest(BaseTestCase):
|
||||
httpbin('/status/500')
|
||||
)
|
||||
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):
|
||||
r = http(
|
||||
@ -811,7 +819,7 @@ class ExitStatusTest(BaseTestCase):
|
||||
env=TestEnvironment(stdout_isatty=False,)
|
||||
)
|
||||
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())
|
||||
|
||||
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.
|
||||
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):
|
||||
r = http(
|
||||
@ -832,7 +840,7 @@ class ExitStatusTest(BaseTestCase):
|
||||
httpbin('/status/401')
|
||||
)
|
||||
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.
|
||||
self.assertTrue(not r.stderr)
|
||||
|
||||
@ -843,7 +851,7 @@ class ExitStatusTest(BaseTestCase):
|
||||
httpbin('/status/500')
|
||||
)
|
||||
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):
|
||||
@ -855,7 +863,7 @@ class FakeWindowsTest(BaseTestCase):
|
||||
httpbin('/get'),
|
||||
env=env
|
||||
)
|
||||
self.assertNotEqual(r.exit_status, 0)
|
||||
self.assertNotEqual(r.exit_status, EXIT.OK)
|
||||
self.assertIn('Windows', r.stderr)
|
||||
self.assertIn('--output', r.stderr)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user