mirror of
https://github.com/httpie/cli.git
synced 2025-08-14 12:49:16 +02:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
809a461a26 | |||
c3d550e930 | |||
172df162b3 | |||
1bad62ab0e | |||
8d302f91f9 | |||
63b61bc811 | |||
5af88756a6 | |||
7f624e61b5 | |||
6e848b3203 | |||
8e112a6948 | |||
87c59ae561 | |||
76eebeac2a | |||
2e57c080fd |
@ -28,3 +28,4 @@ Patches and ideas
|
|||||||
* `Tomek Wójcik <https://github.com/tomekwojcik>`_
|
* `Tomek Wójcik <https://github.com/tomekwojcik>`_
|
||||||
* `Davey Shafik <https://github.com/dshafik>`_
|
* `Davey Shafik <https://github.com/dshafik>`_
|
||||||
* `cido <https://github.com/cido>`_
|
* `cido <https://github.com/cido>`_
|
||||||
|
* `Justin Bonnar <https://github.com/jargonjustin>`_
|
||||||
|
47
README.rst
47
README.rst
@ -181,6 +181,13 @@ between requests to the same host:
|
|||||||
|
|
||||||
$ http --session=logged-in httpbin.org/headers
|
$ http --session=logged-in httpbin.org/headers
|
||||||
|
|
||||||
|
|
||||||
|
Set a custom ``Host`` header to work around missing DNS records:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http localhost:8000 Host:example.com
|
||||||
|
|
||||||
..
|
..
|
||||||
|
|
||||||
--------
|
--------
|
||||||
@ -744,6 +751,7 @@ Also, the following formatting is applied:
|
|||||||
* HTTP headers are sorted by name.
|
* HTTP headers are sorted by name.
|
||||||
* JSON data is indented, sorted by keys, and unicode escapes are converted
|
* JSON data is indented, sorted by keys, and unicode escapes are converted
|
||||||
to the characters they represent.
|
to the characters they represent.
|
||||||
|
* XML data is indented for better readability.
|
||||||
|
|
||||||
One of these options can be used to control output processing:
|
One of these options can be used to control output processing:
|
||||||
|
|
||||||
@ -937,13 +945,17 @@ Streamed output by small chunks alá ``tail -f``:
|
|||||||
Sessions
|
Sessions
|
||||||
========
|
========
|
||||||
|
|
||||||
By default, every request is completely independent of the previous ones.
|
By default, every request is completely independent of any previous ones.
|
||||||
HTTPie also supports persistent sessions, where custom headers (except for the
|
HTTPie also supports persistent sessions, where custom headers (except for the
|
||||||
ones starting with ``Content-`` or ``If-``), authorization, and cookies
|
ones starting with ``Content-`` or ``If-``), authorization, and cookies
|
||||||
(manually specified or sent by the server) persist between requests
|
(manually specified or sent by the server) persist between requests
|
||||||
to the same host.
|
to the same host.
|
||||||
|
|
||||||
Create a new session named ``user1``:
|
--------------
|
||||||
|
Named Sessions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Create a new session named ``user1`` for ``example.org``:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@ -966,14 +978,30 @@ To use a session without updating it from the request/response exchange
|
|||||||
once it is created, specify the session name via
|
once it is created, specify the session name via
|
||||||
``--session-read-only=SESSION_NAME`` instead.
|
``--session-read-only=SESSION_NAME`` instead.
|
||||||
|
|
||||||
Session data are stored in JSON files in the directory
|
Named sessions' data is stored in JSON files in the directory
|
||||||
``~/.httpie/sessions/<host>/<name>.json``
|
``~/.httpie/sessions/<host>/<name>.json``
|
||||||
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
|
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
|
||||||
|
|
||||||
|
------------------
|
||||||
|
Anonymous Sessions
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Instead of a name, you can also directly specify a path to a session file. This
|
||||||
|
allows for sessions to be re-used across multiple hosts:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http --session=/tmp/session.json example.org
|
||||||
|
$ http --session=/tmp/session.json admin.example.org
|
||||||
|
$ http --session=~/.httpie/sessions/another.example.org/test.json example.org
|
||||||
|
$ http --session-read-only=/tmp/session.json example.org
|
||||||
|
|
||||||
|
|
||||||
**Warning:** All session data, including credentials, cookie data,
|
**Warning:** All session data, including credentials, cookie data,
|
||||||
and custom headers are stored in plain text.
|
and custom headers are stored in plain text.
|
||||||
|
|
||||||
Session files can also be created and edited manually in a text editor.
|
Note that session files can also be created and edited manually in a text
|
||||||
|
editor; they are plain JSON.
|
||||||
|
|
||||||
See also `Config`_.
|
See also `Config`_.
|
||||||
|
|
||||||
@ -1163,7 +1191,11 @@ Changelog
|
|||||||
|
|
||||||
*You can click a version name to see a diff with the previous one.*
|
*You can click a version name to see a diff with the previous one.*
|
||||||
|
|
||||||
* `0.6.0-dev`_
|
* `0.7.0-dev`_
|
||||||
|
* `0.6.0`_
|
||||||
|
* XML data is now formatted.
|
||||||
|
* ``--session`` and ``--session-read-only`` now also accept paths to
|
||||||
|
session files (eg. ``http --session=/tmp/session.json example.org``).
|
||||||
* `0.5.1`_ (2013-05-13)
|
* `0.5.1`_ (2013-05-13)
|
||||||
* ``Content-*`` and ``If-*`` request headers are not stored in sessions
|
* ``Content-*`` and ``If-*`` request headers are not stored in sessions
|
||||||
anymore as they are request-specific.
|
anymore as they are request-specific.
|
||||||
@ -1277,6 +1309,7 @@ Changelog
|
|||||||
.. _0.4.1: https://github.com/jkbr/httpie/compare/0.4.0...0.4.1
|
.. _0.4.1: https://github.com/jkbr/httpie/compare/0.4.0...0.4.1
|
||||||
.. _0.5.0: https://github.com/jkbr/httpie/compare/0.4.1...0.5.0
|
.. _0.5.0: https://github.com/jkbr/httpie/compare/0.4.1...0.5.0
|
||||||
.. _0.5.1: https://github.com/jkbr/httpie/compare/0.5.0...0.5.1
|
.. _0.5.1: https://github.com/jkbr/httpie/compare/0.5.0...0.5.1
|
||||||
.. _0.6.0-dev: https://github.com/jkbr/httpie/compare/0.5.1...master
|
.. _0.6.0: https://github.com/jkbr/httpie/compare/0.5.1...0.6.0
|
||||||
|
.. _0.7.0-dev: https://github.com/jkbr/httpie/compare/0.6.0...master
|
||||||
.. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
|
.. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
|
||||||
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE
|
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE
|
||||||
|
@ -3,7 +3,7 @@ HTTPie - a CLI, cURL-like tool for humans.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
__author__ = 'Jakub Roztocil'
|
__author__ = 'Jakub Roztocil'
|
||||||
__version__ = '0.5.1'
|
__version__ = '0.6.0'
|
||||||
__licence__ = 'BSD'
|
__licence__ = 'BSD'
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,13 +7,13 @@ from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
|
|||||||
|
|
||||||
from . import __doc__
|
from . import __doc__
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .sessions import DEFAULT_SESSIONS_DIR, Session
|
from .sessions import DEFAULT_SESSIONS_DIR
|
||||||
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
||||||
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
||||||
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
|
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
|
||||||
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
||||||
OUT_RESP_BODY, OUTPUT_OPTIONS,
|
OUT_RESP_BODY, OUTPUT_OPTIONS,
|
||||||
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RegexValidator)
|
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator)
|
||||||
|
|
||||||
|
|
||||||
def _(text):
|
def _(text):
|
||||||
@ -256,19 +256,21 @@ output_options.add_argument(
|
|||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Sessions
|
# Sessions
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
sessions = parser.add_argument_group(title='Sessions')\
|
sessions = parser.add_argument_group(title='Sessions')\
|
||||||
.add_mutually_exclusive_group(required=False)
|
.add_mutually_exclusive_group(required=False)
|
||||||
|
|
||||||
|
session_name_validator = SessionNameValidator(
|
||||||
|
'Session name contains invalid characters.')
|
||||||
|
|
||||||
sessions.add_argument(
|
sessions.add_argument(
|
||||||
'--session',
|
'--session',
|
||||||
metavar='SESSION_NAME',
|
metavar='SESSION_NAME_OR_PATH',
|
||||||
type=RegexValidator(
|
type=session_name_validator,
|
||||||
Session.VALID_NAME_PATTERN,
|
|
||||||
'Session name contains invalid characters.'
|
|
||||||
),
|
|
||||||
help=_('''
|
help=_('''
|
||||||
Create, or reuse and update a session.
|
Create, or reuse and update a session.
|
||||||
Within a session, custom headers, auth credential, as well as any
|
Within a session, custom headers, auth credential, as well as any
|
||||||
@ -278,7 +280,8 @@ sessions.add_argument(
|
|||||||
)
|
)
|
||||||
sessions.add_argument(
|
sessions.add_argument(
|
||||||
'--session-read-only',
|
'--session-read-only',
|
||||||
metavar='SESSION_NAME',
|
metavar='SESSION_NAME_OR_PATH',
|
||||||
|
type=session_name_validator,
|
||||||
help=_('''
|
help=_('''
|
||||||
Create or read a session without updating it form the
|
Create or read a session without updating it form the
|
||||||
request/response exchange.
|
request/response exchange.
|
||||||
@ -289,6 +292,7 @@ sessions.add_argument(
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# Authentication
|
# Authentication
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# ``requests.request`` keyword arguments.
|
# ``requests.request`` keyword arguments.
|
||||||
auth = parser.add_argument_group(title='Authentication')
|
auth = parser.add_argument_group(title='Authentication')
|
||||||
auth.add_argument(
|
auth.add_argument(
|
||||||
@ -312,8 +316,9 @@ auth.add_argument(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
# Network
|
# Network
|
||||||
#############################################
|
###############################################################################
|
||||||
|
|
||||||
network = parser.add_argument_group(title='Network')
|
network = parser.add_argument_group(title='Network')
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ def get_response(args, config_dir):
|
|||||||
else:
|
else:
|
||||||
response = sessions.get_response(
|
response = sessions.get_response(
|
||||||
config_dir=config_dir,
|
config_dir=config_dir,
|
||||||
name=args.session or args.session_read_only,
|
session_name=args.session or args.session_read_only,
|
||||||
requests_kwargs=requests_kwargs,
|
requests_kwargs=requests_kwargs,
|
||||||
read_only=bool(args.session_read_only),
|
read_only=bool(args.session_read_only),
|
||||||
)
|
)
|
||||||
|
@ -16,9 +16,8 @@ DEFAULT_CONFIG_DIR = os.environ.get(
|
|||||||
class BaseConfigDict(dict):
|
class BaseConfigDict(dict):
|
||||||
|
|
||||||
name = None
|
name = None
|
||||||
help = None
|
helpurl = None
|
||||||
about = None
|
about = None
|
||||||
|
|
||||||
directory = DEFAULT_CONFIG_DIR
|
directory = DEFAULT_CONFIG_DIR
|
||||||
|
|
||||||
def __init__(self, directory=None, *args, **kwargs):
|
def __init__(self, directory=None, *args, **kwargs):
|
||||||
@ -29,18 +28,24 @@ class BaseConfigDict(dict):
|
|||||||
def __getattr__(self, item):
|
def __getattr__(self, item):
|
||||||
return self[item]
|
return self[item]
|
||||||
|
|
||||||
@property
|
def _get_path(self):
|
||||||
def path(self):
|
"""Return the config file path without side-effects."""
|
||||||
try:
|
|
||||||
os.makedirs(self.directory, mode=0o700)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.EEXIST:
|
|
||||||
raise
|
|
||||||
return os.path.join(self.directory, self.name + '.json')
|
return os.path.join(self.directory, self.name + '.json')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
"""Return the config file path creating basedir, if needed."""
|
||||||
|
path = self._get_path()
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(path), mode=0o700)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
return path
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_new(self):
|
def is_new(self):
|
||||||
return not os.path.exists(self.path)
|
return not os.path.exists(self._get_path())
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
try:
|
try:
|
||||||
@ -61,8 +66,8 @@ class BaseConfigDict(dict):
|
|||||||
self['__meta__'] = {
|
self['__meta__'] = {
|
||||||
'httpie': __version__
|
'httpie': __version__
|
||||||
}
|
}
|
||||||
if self.help:
|
if self.helpurl:
|
||||||
self['__meta__']['help'] = self.help
|
self['__meta__']['help'] = self.helpurl
|
||||||
|
|
||||||
if self.about:
|
if self.about:
|
||||||
self['__meta__']['about'] = self.about
|
self['__meta__']['about'] = self.about
|
||||||
@ -82,7 +87,7 @@ class BaseConfigDict(dict):
|
|||||||
class Config(BaseConfigDict):
|
class Config(BaseConfigDict):
|
||||||
|
|
||||||
name = 'config'
|
name = 'config'
|
||||||
help = 'https://github.com/jkbr/httpie#config'
|
helpurl = 'https://github.com/jkbr/httpie#config'
|
||||||
about = 'HTTPie configuration file'
|
about = 'HTTPie configuration file'
|
||||||
|
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
|
@ -9,7 +9,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import threading
|
import threading
|
||||||
from time import time
|
from time import sleep, time
|
||||||
|
|
||||||
from .output import RawStream
|
from .output import RawStream
|
||||||
from .models import HTTPResponse
|
from .models import HTTPResponse
|
||||||
@ -163,7 +163,7 @@ class Download(object):
|
|||||||
self.finished = False
|
self.finished = False
|
||||||
|
|
||||||
self._status = Status()
|
self._status = Status()
|
||||||
self._progress_reporter = ProgressReporter(
|
self._progress_reporter = ProgressReporterThread(
|
||||||
status=self._status,
|
status=self._status,
|
||||||
output=progress_file
|
output=progress_file
|
||||||
)
|
)
|
||||||
@ -253,7 +253,7 @@ class Download(object):
|
|||||||
self._output_file.name
|
self._output_file.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self._progress_reporter.report()
|
self._progress_reporter.start()
|
||||||
|
|
||||||
return stream, self._output_file
|
return stream, self._output_file
|
||||||
|
|
||||||
@ -263,7 +263,6 @@ class Download(object):
|
|||||||
self._status.finished()
|
self._status.finished()
|
||||||
|
|
||||||
def failed(self):
|
def failed(self):
|
||||||
self.finish()
|
|
||||||
self._progress_reporter.stop()
|
self._progress_reporter.stop()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -317,7 +316,7 @@ class Status(object):
|
|||||||
self.time_finished = time()
|
self.time_finished = time()
|
||||||
|
|
||||||
|
|
||||||
class ProgressReporter(object):
|
class ProgressReporterThread(threading.Thread):
|
||||||
"""
|
"""
|
||||||
Reports download progress based on its status.
|
Reports download progress based on its status.
|
||||||
|
|
||||||
@ -330,6 +329,7 @@ class ProgressReporter(object):
|
|||||||
:type status: Status
|
:type status: Status
|
||||||
:type output: file
|
:type output: file
|
||||||
"""
|
"""
|
||||||
|
super(ProgressReporterThread, self).__init__()
|
||||||
self.status = status
|
self.status = status
|
||||||
self.output = output
|
self.output = output
|
||||||
self._tick = tick
|
self._tick = tick
|
||||||
@ -338,20 +338,20 @@ class ProgressReporter(object):
|
|||||||
self._status_line = ''
|
self._status_line = ''
|
||||||
self._prev_bytes = 0
|
self._prev_bytes = 0
|
||||||
self._prev_time = time()
|
self._prev_time = time()
|
||||||
self._stop = False
|
self._should_stop = threading.Event()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop reporting on next tick."""
|
"""Stop reporting on next tick."""
|
||||||
self._stop = True
|
self._should_stop.set()
|
||||||
|
|
||||||
def report(self):
|
def run(self):
|
||||||
if self._stop:
|
while not self._should_stop.is_set():
|
||||||
return
|
|
||||||
if self.status.has_finished:
|
if self.status.has_finished:
|
||||||
self.sum_up()
|
self.sum_up()
|
||||||
else:
|
break
|
||||||
|
|
||||||
self.report_speed()
|
self.report_speed()
|
||||||
threading.Timer(self._tick, self.report).start()
|
sleep(self._tick)
|
||||||
|
|
||||||
def report_speed(self):
|
def report_speed(self):
|
||||||
|
|
||||||
@ -382,7 +382,7 @@ class ProgressReporter(object):
|
|||||||
s = int((self.status.total_size - downloaded) / speed)
|
s = int((self.status.total_size - downloaded) / speed)
|
||||||
h, s = divmod(s, 60 * 60)
|
h, s = divmod(s, 60 * 60)
|
||||||
m, s = divmod(s, 60)
|
m, s = divmod(s, 60)
|
||||||
eta = '{}:{:0>2}:{:0>2}'.format(h, m, s)
|
eta = '{0}:{1:0>2}:{2:0>2}'.format(h, m, s)
|
||||||
|
|
||||||
self._status_line = PROGRESS.format(
|
self._status_line = PROGRESS.format(
|
||||||
percentage=percentage,
|
percentage=percentage,
|
||||||
|
@ -20,6 +20,7 @@ except ImportError:
|
|||||||
from requests.structures import CaseInsensitiveDict
|
from requests.structures import CaseInsensitiveDict
|
||||||
|
|
||||||
from .compat import urlsplit, str
|
from .compat import urlsplit, str
|
||||||
|
from .sessions import VALID_SESSION_NAME_PATTERN
|
||||||
|
|
||||||
|
|
||||||
HTTP_POST = 'POST'
|
HTTP_POST = 'POST'
|
||||||
@ -373,24 +374,15 @@ class KeyValue(object):
|
|||||||
return self.__dict__ == other.__dict__
|
return self.__dict__ == other.__dict__
|
||||||
|
|
||||||
|
|
||||||
def session_name_arg_type(name):
|
class SessionNameValidator(object):
|
||||||
from .sessions import Session
|
|
||||||
if not Session.is_valid_name(name):
|
|
||||||
raise ArgumentTypeError(
|
|
||||||
'special characters and spaces are not'
|
|
||||||
' allowed in session names: "%s"'
|
|
||||||
% name)
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
def __init__(self, error_message):
|
||||||
class RegexValidator(object):
|
|
||||||
|
|
||||||
def __init__(self, pattern, error_message):
|
|
||||||
self.pattern = re.compile(pattern)
|
|
||||||
self.error_message = error_message
|
self.error_message = error_message
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
if not self.pattern.search(value):
|
# Session name can be a path or just a name.
|
||||||
|
if (os.path.sep not in value
|
||||||
|
and not VALID_SESSION_NAME_PATTERN.search(value)):
|
||||||
raise ArgumentError(None, self.error_message)
|
raise ArgumentError(None, self.error_message)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import xml.dom.minidom
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
@ -20,6 +21,9 @@ from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
|
|||||||
OUT_RESP_HEAD, OUT_RESP_BODY)
|
OUT_RESP_HEAD, OUT_RESP_BODY)
|
||||||
|
|
||||||
|
|
||||||
|
# The default number of spaces to indent when pretty printing
|
||||||
|
DEFAULT_INDENT = 4
|
||||||
|
|
||||||
# Colors on Windows via colorama don't look that
|
# Colors on Windows via colorama don't look that
|
||||||
# great and fruity seems to give the best result there.
|
# great and fruity seems to give the best result there.
|
||||||
AVAILABLE_STYLES = set(STYLE_MAP.keys())
|
AVAILABLE_STYLES = set(STYLE_MAP.keys())
|
||||||
@ -264,8 +268,9 @@ class PrettyStream(EncodedStream):
|
|||||||
def _process_body(self, chunk):
|
def _process_body(self, chunk):
|
||||||
return (self.processor
|
return (self.processor
|
||||||
.process_body(
|
.process_body(
|
||||||
chunk.decode(self.msg.encoding, 'replace'),
|
content=chunk.decode(self.msg.encoding, 'replace'),
|
||||||
self.msg.content_type)
|
content_type=self.msg.content_type,
|
||||||
|
encoding=self.msg.encoding)
|
||||||
.encode(self.output_encoding, 'replace'))
|
.encode(self.output_encoding, 'replace'))
|
||||||
|
|
||||||
|
|
||||||
@ -367,12 +372,13 @@ class BaseProcessor(object):
|
|||||||
"""
|
"""
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
def process_body(self, content, content_type, subtype):
|
def process_body(self, content, content_type, subtype, encoding):
|
||||||
"""Return processed `content`.
|
"""Return processed `content`.
|
||||||
|
|
||||||
:param content: The body content as text
|
:param content: The body content as text
|
||||||
:param content_type: Full content type, e.g., 'application/atom+xml'.
|
:param content_type: Full content type, e.g., 'application/atom+xml'.
|
||||||
:param subtype: E.g. 'xml'.
|
:param subtype: E.g. 'xml'.
|
||||||
|
:param encoding: The original content encoding.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return content
|
return content
|
||||||
@ -381,7 +387,7 @@ class BaseProcessor(object):
|
|||||||
class JSONProcessor(BaseProcessor):
|
class JSONProcessor(BaseProcessor):
|
||||||
"""JSON body processor."""
|
"""JSON body processor."""
|
||||||
|
|
||||||
def process_body(self, content, content_type, subtype):
|
def process_body(self, content, content_type, subtype, encoding):
|
||||||
if subtype == 'json':
|
if subtype == 'json':
|
||||||
try:
|
try:
|
||||||
# Indent the JSON data, sort keys by name, and
|
# Indent the JSON data, sort keys by name, and
|
||||||
@ -389,13 +395,29 @@ class JSONProcessor(BaseProcessor):
|
|||||||
content = json.dumps(json.loads(content),
|
content = json.dumps(json.loads(content),
|
||||||
sort_keys=True,
|
sort_keys=True,
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
indent=4)
|
indent=DEFAULT_INDENT)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Invalid JSON but we don't care.
|
# Invalid JSON but we don't care.
|
||||||
pass
|
pass
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
class XMLProcessor(BaseProcessor):
|
||||||
|
"""XML body processor."""
|
||||||
|
# TODO: tests
|
||||||
|
|
||||||
|
def process_body(self, content, content_type, subtype, encoding):
|
||||||
|
if subtype == 'xml':
|
||||||
|
try:
|
||||||
|
# Pretty print the XML
|
||||||
|
doc = xml.dom.minidom.parseString(content.encode(encoding))
|
||||||
|
content = doc.toprettyxml(indent=' ' * DEFAULT_INDENT)
|
||||||
|
except xml.parsers.expat.ExpatError:
|
||||||
|
# Ignore invalid XML errors (skips attempting to pretty print)
|
||||||
|
pass
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
class PygmentsProcessor(BaseProcessor):
|
class PygmentsProcessor(BaseProcessor):
|
||||||
"""A processor that applies syntax-highlighting using Pygments
|
"""A processor that applies syntax-highlighting using Pygments
|
||||||
to the headers, and to the body as well if its content type is recognized.
|
to the headers, and to the body as well if its content type is recognized.
|
||||||
@ -427,7 +449,7 @@ class PygmentsProcessor(BaseProcessor):
|
|||||||
return pygments.highlight(
|
return pygments.highlight(
|
||||||
headers, HTTPLexer(), self.formatter).strip()
|
headers, HTTPLexer(), self.formatter).strip()
|
||||||
|
|
||||||
def process_body(self, content, content_type, subtype):
|
def process_body(self, content, content_type, subtype, encoding):
|
||||||
try:
|
try:
|
||||||
lexer = self.lexers_by_type.get(content_type)
|
lexer = self.lexers_by_type.get(content_type)
|
||||||
if not lexer:
|
if not lexer:
|
||||||
@ -460,7 +482,8 @@ class OutputProcessor(object):
|
|||||||
installed_processors = {
|
installed_processors = {
|
||||||
'format': [
|
'format': [
|
||||||
HeadersProcessor,
|
HeadersProcessor,
|
||||||
JSONProcessor
|
JSONProcessor,
|
||||||
|
XMLProcessor
|
||||||
],
|
],
|
||||||
'colors': [
|
'colors': [
|
||||||
PygmentsProcessor
|
PygmentsProcessor
|
||||||
@ -486,13 +509,18 @@ class OutputProcessor(object):
|
|||||||
headers = processor.process_headers(headers)
|
headers = processor.process_headers(headers)
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
def process_body(self, content, content_type):
|
def process_body(self, content, content_type, encoding):
|
||||||
# e.g., 'application/atom+xml'
|
# e.g., 'application/atom+xml'
|
||||||
content_type = content_type.split(';')[0]
|
content_type = content_type.split(';')[0]
|
||||||
# e.g., 'xml'
|
# e.g., 'xml'
|
||||||
subtype = content_type.split('/')[-1].split('+')[-1]
|
subtype = content_type.split('/')[-1].split('+')[-1]
|
||||||
|
|
||||||
for processor in self.processors:
|
for processor in self.processors:
|
||||||
content = processor.process_body(content, content_type, subtype)
|
content = processor.process_body(
|
||||||
|
content,
|
||||||
|
content_type,
|
||||||
|
subtype,
|
||||||
|
encoding
|
||||||
|
)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
@ -3,9 +3,6 @@
|
|||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import glob
|
|
||||||
import errno
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.cookies import RequestsCookieJar, create_cookie
|
from requests.cookies import RequestsCookieJar, create_cookie
|
||||||
@ -17,34 +14,40 @@ from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
|||||||
|
|
||||||
SESSIONS_DIR_NAME = 'sessions'
|
SESSIONS_DIR_NAME = 'sessions'
|
||||||
DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME)
|
DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME)
|
||||||
|
VALID_SESSION_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
|
||||||
|
|
||||||
# Request headers starting with these prefixes won't be stored in sessions.
|
# Request headers starting with these prefixes won't be stored in sessions.
|
||||||
# They are specific to each request.
|
# They are specific to each request.
|
||||||
# http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests
|
# http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests
|
||||||
SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
|
SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
|
||||||
|
|
||||||
|
|
||||||
def get_response(name, requests_kwargs, config_dir, read_only=False):
|
def get_response(session_name, requests_kwargs, config_dir, read_only=False):
|
||||||
"""Like `client.get_response`, but applies permanent
|
"""Like `client.get_response`, but applies permanent
|
||||||
aspects of the session to the request.
|
aspects of the session to the request.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
sessions_dir = os.path.join(config_dir, SESSIONS_DIR_NAME)
|
if os.path.sep in session_name:
|
||||||
host = Host(
|
path = os.path.expanduser(session_name)
|
||||||
root_dir=sessions_dir,
|
else:
|
||||||
name=requests_kwargs['headers'].get('Host', None)
|
hostname = (
|
||||||
|
requests_kwargs['headers'].get('Host', None)
|
||||||
or urlsplit(requests_kwargs['url']).netloc.split('@')[-1]
|
or urlsplit(requests_kwargs['url']).netloc.split('@')[-1]
|
||||||
)
|
)
|
||||||
session = Session(host, name)
|
|
||||||
|
assert re.match('^[a-zA-Z0-9_.:-]+$', hostname)
|
||||||
|
|
||||||
|
# host:port => host_port
|
||||||
|
hostname = hostname.replace(':', '_')
|
||||||
|
path = os.path.join(config_dir,
|
||||||
|
SESSIONS_DIR_NAME,
|
||||||
|
hostname,
|
||||||
|
session_name + '.json')
|
||||||
|
|
||||||
|
session = Session(path)
|
||||||
session.load()
|
session.load()
|
||||||
|
|
||||||
# Merge request and session headers to get final headers for this request.
|
|
||||||
request_headers = requests_kwargs.get('headers', {})
|
request_headers = requests_kwargs.get('headers', {})
|
||||||
merged_headers = session.headers.copy()
|
requests_kwargs['headers'] = dict(session.headers, **request_headers)
|
||||||
merged_headers.update(request_headers)
|
|
||||||
requests_kwargs['headers'] = merged_headers
|
|
||||||
# Update session headers with the request headers.
|
|
||||||
session.update_headers(request_headers)
|
session.update_headers(request_headers)
|
||||||
|
|
||||||
auth = requests_kwargs.get('auth', None)
|
auth = requests_kwargs.get('auth', None)
|
||||||
@ -68,69 +71,13 @@ def get_response(name, requests_kwargs, config_dir, read_only=False):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class Host(object):
|
|
||||||
"""A host is a per-host directory on the disk containing sessions files."""
|
|
||||||
|
|
||||||
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.:-]+$')
|
|
||||||
|
|
||||||
def __init__(self, name, root_dir=DEFAULT_SESSIONS_DIR):
|
|
||||||
assert self.VALID_NAME_PATTERN.match(name)
|
|
||||||
self.name = name
|
|
||||||
self.root_dir = root_dir
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Return an iterator yielding `Session` instances."""
|
|
||||||
for fn in sorted(glob.glob1(self.path, '*.json')):
|
|
||||||
session_name = os.path.splitext(fn)[0]
|
|
||||||
yield Session(host=self, name=session_name)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _quote_name(name):
|
|
||||||
"""host:port => host_port"""
|
|
||||||
return name.replace(':', '_')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _unquote_name(name):
|
|
||||||
"""host_port => host:port"""
|
|
||||||
return re.sub(r'_(\d+)$', r':\1', name)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def all(cls, root_dir=DEFAULT_SESSIONS_DIR):
|
|
||||||
"""Return a generator yielding a host at a time."""
|
|
||||||
for name in sorted(glob.glob1(root_dir, '*')):
|
|
||||||
if os.path.isdir(os.path.join(root_dir, name)):
|
|
||||||
yield Host(cls._unquote_name(name), root_dir=root_dir)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def verbose_name(self):
|
|
||||||
return '%s %s' % (self.name, self.path)
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
shutil.rmtree(self.path)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self):
|
|
||||||
path = os.path.join(self.root_dir, self._quote_name(self.name))
|
|
||||||
try:
|
|
||||||
os.makedirs(path, mode=0o700)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.EEXIST:
|
|
||||||
raise
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
class Session(BaseConfigDict):
|
class Session(BaseConfigDict):
|
||||||
|
helpurl = 'https://github.com/jkbr/httpie#sessions'
|
||||||
help = 'https://github.com/jkbr/httpie#sessions'
|
|
||||||
about = 'HTTPie session file'
|
about = 'HTTPie session file'
|
||||||
|
|
||||||
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
|
def __init__(self, path, *args, **kwargs):
|
||||||
|
|
||||||
def __init__(self, host, name, *args, **kwargs):
|
|
||||||
assert self.VALID_NAME_PATTERN.match(name)
|
|
||||||
super(Session, self).__init__(*args, **kwargs)
|
super(Session, self).__init__(*args, **kwargs)
|
||||||
self.host = host
|
self._path = path
|
||||||
self.name = name
|
|
||||||
self['headers'] = {}
|
self['headers'] = {}
|
||||||
self['cookies'] = {}
|
self['cookies'] = {}
|
||||||
self['auth'] = {
|
self['auth'] = {
|
||||||
@ -139,13 +86,8 @@ class Session(BaseConfigDict):
|
|||||||
'password': None
|
'password': None
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
def _get_path(self):
|
||||||
def directory(self):
|
return self._path
|
||||||
return self.host.path
|
|
||||||
|
|
||||||
@property
|
|
||||||
def verbose_name(self):
|
|
||||||
return '%s %s %s' % (self.host.name, self.name, self.path)
|
|
||||||
|
|
||||||
def update_headers(self, request_headers):
|
def update_headers(self, request_headers):
|
||||||
"""
|
"""
|
||||||
|
2
setup.py
2
setup.py
@ -12,7 +12,7 @@ if sys.argv[-1] == 'test':
|
|||||||
|
|
||||||
|
|
||||||
requirements = [
|
requirements = [
|
||||||
'requests>=1.0.4',
|
'requests>=1.2.3',
|
||||||
'Pygments>=1.5'
|
'Pygments>=1.5'
|
||||||
]
|
]
|
||||||
if sys.version_info[:2] in ((2, 6), (3, 1)):
|
if sys.version_info[:2] in ((2, 6), (3, 1)):
|
||||||
|
@ -1170,6 +1170,8 @@ class ItemParsingTest(BaseTestCase):
|
|||||||
# files
|
# files
|
||||||
self.key_value_type('bar\\@baz@%s' % FILE_PATH_ARG)
|
self.key_value_type('bar\\@baz@%s' % FILE_PATH_ARG)
|
||||||
])
|
])
|
||||||
|
# `requests.structures.CaseInsensitiveDict` => `dict`
|
||||||
|
headers = dict(headers._store.values())
|
||||||
self.assertDictEqual(headers, {
|
self.assertDictEqual(headers, {
|
||||||
'foo:bar': 'baz',
|
'foo:bar': 'baz',
|
||||||
'jack@jill': 'hill',
|
'jack@jill': 'hill',
|
||||||
@ -1199,6 +1201,8 @@ class ItemParsingTest(BaseTestCase):
|
|||||||
self.key_value_type('test-file@%s' % FILE_PATH_ARG),
|
self.key_value_type('test-file@%s' % FILE_PATH_ARG),
|
||||||
self.key_value_type('query==value'),
|
self.key_value_type('query==value'),
|
||||||
])
|
])
|
||||||
|
# `requests.structures.CaseInsensitiveDict` => `dict`
|
||||||
|
headers = dict(headers._store.values())
|
||||||
self.assertDictEqual(headers, {
|
self.assertDictEqual(headers, {
|
||||||
'header': 'value',
|
'header': 'value',
|
||||||
'eh': ''
|
'eh': ''
|
||||||
@ -1464,6 +1468,28 @@ class SessionTest(BaseTestCase):
|
|||||||
# Should be the same as before r2.
|
# Should be the same as before r2.
|
||||||
self.assertDictEqual(r1.json, r3.json)
|
self.assertDictEqual(r1.json, r3.json)
|
||||||
|
|
||||||
|
def test_session_by_path(self):
|
||||||
|
session_path = os.path.join(self.config_dir, 'session-by-path.json')
|
||||||
|
|
||||||
|
r1 = http(
|
||||||
|
'--session=' + session_path,
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
'Foo:Bar',
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r1)
|
||||||
|
|
||||||
|
r2 = http(
|
||||||
|
'--session=' + session_path,
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r2)
|
||||||
|
|
||||||
|
self.assertEqual(r2.json['headers']['Foo'], 'Bar')
|
||||||
|
|
||||||
|
|
||||||
class DownloadUtilsTest(BaseTestCase):
|
class DownloadUtilsTest(BaseTestCase):
|
||||||
|
|
||||||
@ -1579,9 +1605,23 @@ class DownloadTest(BaseTestCase):
|
|||||||
self.assertIn('Done', r.stderr)
|
self.assertIn('Done', r.stderr)
|
||||||
self.assertEqual(body, r)
|
self.assertEqual(body, r)
|
||||||
|
|
||||||
|
def test_download_with_Content_Length(self):
|
||||||
|
download = Download(output_file=open(os.devnull, 'w'))
|
||||||
|
download.start(Response(
|
||||||
|
url=httpbin('/'),
|
||||||
|
headers={'Content-Length': 10}
|
||||||
|
))
|
||||||
|
time.sleep(1.1)
|
||||||
|
download._chunk_downloaded(b'12345')
|
||||||
|
time.sleep(1.1)
|
||||||
|
download._chunk_downloaded(b'12345')
|
||||||
|
download.finish()
|
||||||
|
self.assertFalse(download.interrupted)
|
||||||
|
|
||||||
def test_download_no_Content_Length(self):
|
def test_download_no_Content_Length(self):
|
||||||
download = Download(output_file=open(os.devnull, 'w'))
|
download = Download(output_file=open(os.devnull, 'w'))
|
||||||
download.start(Response(url=httpbin('/')))
|
download.start(Response(url=httpbin('/')))
|
||||||
|
time.sleep(1.1)
|
||||||
download._chunk_downloaded(b'12345')
|
download._chunk_downloaded(b'12345')
|
||||||
download.finish()
|
download.finish()
|
||||||
self.assertFalse(download.interrupted)
|
self.assertFalse(download.interrupted)
|
||||||
|
Reference in New Issue
Block a user