mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 16:19:11 +02:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
809a461a26 | |||
c3d550e930 | |||
172df162b3 | |||
1bad62ab0e | |||
8d302f91f9 | |||
63b61bc811 | |||
5af88756a6 | |||
7f624e61b5 | |||
6e848b3203 | |||
8e112a6948 | |||
87c59ae561 | |||
76eebeac2a | |||
5b9cbcb530 | |||
8ad33d5f6a | |||
86ac4cdb7b | |||
e09b74021c | |||
2e57c080fd |
@ -28,3 +28,4 @@ Patches and ideas
|
||||
* `Tomek Wójcik <https://github.com/tomekwojcik>`_
|
||||
* `Davey Shafik <https://github.com/dshafik>`_
|
||||
* `cido <https://github.com/cido>`_
|
||||
* `Justin Bonnar <https://github.com/jargonjustin>`_
|
||||
|
58
README.rst
58
README.rst
@ -181,6 +181,13 @@ between requests to the same host:
|
||||
|
||||
$ 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.
|
||||
* JSON data is indented, sorted by keys, and unicode escapes are converted
|
||||
to the characters they represent.
|
||||
* XML data is indented for better readability.
|
||||
|
||||
One of these options can be used to control output processing:
|
||||
|
||||
@ -937,12 +945,17 @@ Streamed output by small chunks alá ``tail -f``:
|
||||
Sessions
|
||||
========
|
||||
|
||||
By default, every request is completely independent of the previous ones.
|
||||
HTTPie also supports persistent sessions, where custom headers, authorization,
|
||||
and cookies (manually specified or sent by the server) persist between
|
||||
requests to the same host.
|
||||
By default, every request is completely independent of any previous ones.
|
||||
HTTPie also supports persistent sessions, where custom headers (except for the
|
||||
ones starting with ``Content-`` or ``If-``), authorization, and cookies
|
||||
(manually specified or sent by the server) persist between requests
|
||||
to the same host.
|
||||
|
||||
Create a new session named ``user1``:
|
||||
--------------
|
||||
Named Sessions
|
||||
--------------
|
||||
|
||||
Create a new session named ``user1`` for ``example.org``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@ -965,14 +978,30 @@ To use a session without updating it from the request/response exchange
|
||||
once it is created, specify the session name via
|
||||
``--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``
|
||||
(``%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,
|
||||
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`_.
|
||||
|
||||
@ -1162,7 +1191,14 @@ Changelog
|
||||
|
||||
*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)
|
||||
* ``Content-*`` and ``If-*`` request headers are not stored in sessions
|
||||
anymore as they are request-specific.
|
||||
* `0.5.0`_ (2013-04-27)
|
||||
* Added a `download mode`_ via ``--download``.
|
||||
* Bugfixes.
|
||||
@ -1272,6 +1308,8 @@ Changelog
|
||||
.. _0.4.0: https://github.com/jkbr/httpie/compare/0.3.0...0.4.0
|
||||
.. _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.6.0-dev: https://github.com/jkbr/httpie/compare/0.5.0...master
|
||||
.. _0.5.1: https://github.com/jkbr/httpie/compare/0.5.0...0.5.1
|
||||
.. _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
|
||||
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE
|
||||
|
@ -3,7 +3,7 @@ HTTPie - a CLI, cURL-like tool for humans.
|
||||
|
||||
"""
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__version__ = '0.5.0'
|
||||
__version__ = '0.6.0'
|
||||
__licence__ = 'BSD'
|
||||
|
||||
|
||||
|
@ -7,13 +7,13 @@ from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
|
||||
|
||||
from . import __doc__
|
||||
from . import __version__
|
||||
from .sessions import DEFAULT_SESSIONS_DIR, Session
|
||||
from .sessions import DEFAULT_SESSIONS_DIR
|
||||
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
||||
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
||||
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
|
||||
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
||||
OUT_RESP_BODY, OUTPUT_OPTIONS,
|
||||
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RegexValidator)
|
||||
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator)
|
||||
|
||||
|
||||
def _(text):
|
||||
@ -256,19 +256,21 @@ output_options.add_argument(
|
||||
''')
|
||||
)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Sessions
|
||||
###############################################################################
|
||||
|
||||
sessions = parser.add_argument_group(title='Sessions')\
|
||||
.add_mutually_exclusive_group(required=False)
|
||||
|
||||
session_name_validator = SessionNameValidator(
|
||||
'Session name contains invalid characters.')
|
||||
|
||||
sessions.add_argument(
|
||||
'--session',
|
||||
metavar='SESSION_NAME',
|
||||
type=RegexValidator(
|
||||
Session.VALID_NAME_PATTERN,
|
||||
'Session name contains invalid characters.'
|
||||
),
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help=_('''
|
||||
Create, or reuse and update a session.
|
||||
Within a session, custom headers, auth credential, as well as any
|
||||
@ -278,7 +280,8 @@ sessions.add_argument(
|
||||
)
|
||||
sessions.add_argument(
|
||||
'--session-read-only',
|
||||
metavar='SESSION_NAME',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help=_('''
|
||||
Create or read a session without updating it form the
|
||||
request/response exchange.
|
||||
@ -289,6 +292,7 @@ sessions.add_argument(
|
||||
###############################################################################
|
||||
# Authentication
|
||||
###############################################################################
|
||||
|
||||
# ``requests.request`` keyword arguments.
|
||||
auth = parser.add_argument_group(title='Authentication')
|
||||
auth.add_argument(
|
||||
@ -312,8 +316,9 @@ auth.add_argument(
|
||||
)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Network
|
||||
#############################################
|
||||
###############################################################################
|
||||
|
||||
network = parser.add_argument_group(title='Network')
|
||||
|
||||
|
@ -28,8 +28,8 @@ def get_response(args, config_dir):
|
||||
else:
|
||||
response = sessions.get_response(
|
||||
config_dir=config_dir,
|
||||
name=args.session or args.session_read_only,
|
||||
request_kwargs=requests_kwargs,
|
||||
session_name=args.session or args.session_read_only,
|
||||
requests_kwargs=requests_kwargs,
|
||||
read_only=bool(args.session_read_only),
|
||||
)
|
||||
|
||||
|
@ -16,9 +16,8 @@ DEFAULT_CONFIG_DIR = os.environ.get(
|
||||
class BaseConfigDict(dict):
|
||||
|
||||
name = None
|
||||
help = None
|
||||
helpurl = None
|
||||
about = None
|
||||
|
||||
directory = DEFAULT_CONFIG_DIR
|
||||
|
||||
def __init__(self, directory=None, *args, **kwargs):
|
||||
@ -29,18 +28,24 @@ class BaseConfigDict(dict):
|
||||
def __getattr__(self, item):
|
||||
return self[item]
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
try:
|
||||
os.makedirs(self.directory, mode=0o700)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
def _get_path(self):
|
||||
"""Return the config file path without side-effects."""
|
||||
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
|
||||
def is_new(self):
|
||||
return not os.path.exists(self.path)
|
||||
return not os.path.exists(self._get_path())
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
@ -61,8 +66,8 @@ class BaseConfigDict(dict):
|
||||
self['__meta__'] = {
|
||||
'httpie': __version__
|
||||
}
|
||||
if self.help:
|
||||
self['__meta__']['help'] = self.help
|
||||
if self.helpurl:
|
||||
self['__meta__']['help'] = self.helpurl
|
||||
|
||||
if self.about:
|
||||
self['__meta__']['about'] = self.about
|
||||
@ -82,7 +87,7 @@ class BaseConfigDict(dict):
|
||||
class Config(BaseConfigDict):
|
||||
|
||||
name = 'config'
|
||||
help = 'https://github.com/jkbr/httpie#config'
|
||||
helpurl = 'https://github.com/jkbr/httpie#config'
|
||||
about = 'HTTPie configuration file'
|
||||
|
||||
DEFAULTS = {
|
||||
|
@ -9,7 +9,7 @@ import re
|
||||
import sys
|
||||
import mimetypes
|
||||
import threading
|
||||
from time import time
|
||||
from time import sleep, time
|
||||
|
||||
from .output import RawStream
|
||||
from .models import HTTPResponse
|
||||
@ -163,7 +163,7 @@ class Download(object):
|
||||
self.finished = False
|
||||
|
||||
self._status = Status()
|
||||
self._progress_reporter = ProgressReporter(
|
||||
self._progress_reporter = ProgressReporterThread(
|
||||
status=self._status,
|
||||
output=progress_file
|
||||
)
|
||||
@ -253,7 +253,7 @@ class Download(object):
|
||||
self._output_file.name
|
||||
)
|
||||
)
|
||||
self._progress_reporter.report()
|
||||
self._progress_reporter.start()
|
||||
|
||||
return stream, self._output_file
|
||||
|
||||
@ -263,7 +263,6 @@ class Download(object):
|
||||
self._status.finished()
|
||||
|
||||
def failed(self):
|
||||
self.finish()
|
||||
self._progress_reporter.stop()
|
||||
|
||||
@property
|
||||
@ -317,7 +316,7 @@ class Status(object):
|
||||
self.time_finished = time()
|
||||
|
||||
|
||||
class ProgressReporter(object):
|
||||
class ProgressReporterThread(threading.Thread):
|
||||
"""
|
||||
Reports download progress based on its status.
|
||||
|
||||
@ -330,6 +329,7 @@ class ProgressReporter(object):
|
||||
:type status: Status
|
||||
:type output: file
|
||||
"""
|
||||
super(ProgressReporterThread, self).__init__()
|
||||
self.status = status
|
||||
self.output = output
|
||||
self._tick = tick
|
||||
@ -338,20 +338,20 @@ class ProgressReporter(object):
|
||||
self._status_line = ''
|
||||
self._prev_bytes = 0
|
||||
self._prev_time = time()
|
||||
self._stop = False
|
||||
self._should_stop = threading.Event()
|
||||
|
||||
def stop(self):
|
||||
"""Stop reporting on next tick."""
|
||||
self._stop = True
|
||||
self._should_stop.set()
|
||||
|
||||
def run(self):
|
||||
while not self._should_stop.is_set():
|
||||
if self.status.has_finished:
|
||||
self.sum_up()
|
||||
break
|
||||
|
||||
def report(self):
|
||||
if self._stop:
|
||||
return
|
||||
if self.status.has_finished:
|
||||
self.sum_up()
|
||||
else:
|
||||
self.report_speed()
|
||||
threading.Timer(self._tick, self.report).start()
|
||||
sleep(self._tick)
|
||||
|
||||
def report_speed(self):
|
||||
|
||||
@ -382,7 +382,7 @@ class ProgressReporter(object):
|
||||
s = int((self.status.total_size - downloaded) / speed)
|
||||
h, s = divmod(s, 60 * 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(
|
||||
percentage=percentage,
|
||||
|
@ -20,6 +20,7 @@ except ImportError:
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from .compat import urlsplit, str
|
||||
from .sessions import VALID_SESSION_NAME_PATTERN
|
||||
|
||||
|
||||
HTTP_POST = 'POST'
|
||||
@ -373,24 +374,15 @@ class KeyValue(object):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
|
||||
def session_name_arg_type(name):
|
||||
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
|
||||
class SessionNameValidator(object):
|
||||
|
||||
|
||||
class RegexValidator(object):
|
||||
|
||||
def __init__(self, pattern, error_message):
|
||||
self.pattern = re.compile(pattern)
|
||||
def __init__(self, error_message):
|
||||
self.error_message = error_message
|
||||
|
||||
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)
|
||||
return value
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
"""
|
||||
import json
|
||||
import xml.dom.minidom
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
|
||||
@ -20,6 +21,9 @@ from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||
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
|
||||
# great and fruity seems to give the best result there.
|
||||
AVAILABLE_STYLES = set(STYLE_MAP.keys())
|
||||
@ -264,8 +268,9 @@ class PrettyStream(EncodedStream):
|
||||
def _process_body(self, chunk):
|
||||
return (self.processor
|
||||
.process_body(
|
||||
chunk.decode(self.msg.encoding, 'replace'),
|
||||
self.msg.content_type)
|
||||
content=chunk.decode(self.msg.encoding, 'replace'),
|
||||
content_type=self.msg.content_type,
|
||||
encoding=self.msg.encoding)
|
||||
.encode(self.output_encoding, 'replace'))
|
||||
|
||||
|
||||
@ -367,12 +372,13 @@ class BaseProcessor(object):
|
||||
"""
|
||||
return headers
|
||||
|
||||
def process_body(self, content, content_type, subtype):
|
||||
def process_body(self, content, content_type, subtype, encoding):
|
||||
"""Return processed `content`.
|
||||
|
||||
:param content: The body content as text
|
||||
:param content_type: Full content type, e.g., 'application/atom+xml'.
|
||||
:param subtype: E.g. 'xml'.
|
||||
:param encoding: The original content encoding.
|
||||
|
||||
"""
|
||||
return content
|
||||
@ -381,7 +387,7 @@ class BaseProcessor(object):
|
||||
class JSONProcessor(BaseProcessor):
|
||||
"""JSON body processor."""
|
||||
|
||||
def process_body(self, content, content_type, subtype):
|
||||
def process_body(self, content, content_type, subtype, encoding):
|
||||
if subtype == 'json':
|
||||
try:
|
||||
# Indent the JSON data, sort keys by name, and
|
||||
@ -389,13 +395,29 @@ class JSONProcessor(BaseProcessor):
|
||||
content = json.dumps(json.loads(content),
|
||||
sort_keys=True,
|
||||
ensure_ascii=False,
|
||||
indent=4)
|
||||
indent=DEFAULT_INDENT)
|
||||
except ValueError:
|
||||
# Invalid JSON but we don't care.
|
||||
pass
|
||||
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):
|
||||
"""A processor that applies syntax-highlighting using Pygments
|
||||
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(
|
||||
headers, HTTPLexer(), self.formatter).strip()
|
||||
|
||||
def process_body(self, content, content_type, subtype):
|
||||
def process_body(self, content, content_type, subtype, encoding):
|
||||
try:
|
||||
lexer = self.lexers_by_type.get(content_type)
|
||||
if not lexer:
|
||||
@ -460,7 +482,8 @@ class OutputProcessor(object):
|
||||
installed_processors = {
|
||||
'format': [
|
||||
HeadersProcessor,
|
||||
JSONProcessor
|
||||
JSONProcessor,
|
||||
XMLProcessor
|
||||
],
|
||||
'colors': [
|
||||
PygmentsProcessor
|
||||
@ -486,13 +509,18 @@ class OutputProcessor(object):
|
||||
headers = processor.process_headers(headers)
|
||||
return headers
|
||||
|
||||
def process_body(self, content, content_type):
|
||||
def process_body(self, content, content_type, encoding):
|
||||
# e.g., 'application/atom+xml'
|
||||
content_type = content_type.split(';')[0]
|
||||
# e.g., 'xml'
|
||||
subtype = content_type.split('/')[-1].split('+')[-1]
|
||||
|
||||
for processor in self.processors:
|
||||
content = processor.process_body(content, content_type, subtype)
|
||||
content = processor.process_body(
|
||||
content,
|
||||
content_type,
|
||||
subtype,
|
||||
encoding
|
||||
)
|
||||
|
||||
return content
|
||||
|
@ -3,9 +3,6 @@
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
import glob
|
||||
import errno
|
||||
import shutil
|
||||
|
||||
import requests
|
||||
from requests.cookies import RequestsCookieJar, create_cookie
|
||||
@ -17,38 +14,53 @@ from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
||||
|
||||
SESSIONS_DIR_NAME = 'sessions'
|
||||
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.
|
||||
# They are specific to each request.
|
||||
# http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests
|
||||
SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
|
||||
|
||||
|
||||
def get_response(name, request_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
|
||||
aspects of the session to the request.
|
||||
|
||||
"""
|
||||
sessions_dir = os.path.join(config_dir, SESSIONS_DIR_NAME)
|
||||
host = Host(
|
||||
root_dir=sessions_dir,
|
||||
name=request_kwargs['headers'].get('Host', None)
|
||||
or urlsplit(request_kwargs['url']).netloc.split('@')[-1]
|
||||
)
|
||||
session = Session(host, name)
|
||||
if os.path.sep in session_name:
|
||||
path = os.path.expanduser(session_name)
|
||||
else:
|
||||
hostname = (
|
||||
requests_kwargs['headers'].get('Host', None)
|
||||
or urlsplit(requests_kwargs['url']).netloc.split('@')[-1]
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
# Update session headers with the request headers.
|
||||
session['headers'].update(request_kwargs.get('headers', {}))
|
||||
# Use the merged headers for the request
|
||||
request_kwargs['headers'] = session['headers']
|
||||
request_headers = requests_kwargs.get('headers', {})
|
||||
requests_kwargs['headers'] = dict(session.headers, **request_headers)
|
||||
session.update_headers(request_headers)
|
||||
|
||||
auth = request_kwargs.get('auth', None)
|
||||
auth = requests_kwargs.get('auth', None)
|
||||
if auth:
|
||||
session.auth = auth
|
||||
elif session.auth:
|
||||
request_kwargs['auth'] = session.auth
|
||||
requests_kwargs['auth'] = session.auth
|
||||
|
||||
requests_session = requests.Session()
|
||||
requests_session.cookies = session.cookies
|
||||
|
||||
try:
|
||||
response = requests_session.request(**request_kwargs)
|
||||
response = requests_session.request(**requests_kwargs)
|
||||
except Exception:
|
||||
raise
|
||||
else:
|
||||
@ -59,69 +71,13 @@ def get_response(name, request_kwargs, config_dir, read_only=False):
|
||||
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):
|
||||
|
||||
help = 'https://github.com/jkbr/httpie#sessions'
|
||||
helpurl = 'https://github.com/jkbr/httpie#sessions'
|
||||
about = 'HTTPie session file'
|
||||
|
||||
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
|
||||
|
||||
def __init__(self, host, name, *args, **kwargs):
|
||||
assert self.VALID_NAME_PATTERN.match(name)
|
||||
def __init__(self, path, *args, **kwargs):
|
||||
super(Session, self).__init__(*args, **kwargs)
|
||||
self.host = host
|
||||
self.name = name
|
||||
self._path = path
|
||||
self['headers'] = {}
|
||||
self['cookies'] = {}
|
||||
self['auth'] = {
|
||||
@ -130,13 +86,31 @@ class Session(BaseConfigDict):
|
||||
'password': None
|
||||
}
|
||||
|
||||
@property
|
||||
def directory(self):
|
||||
return self.host.path
|
||||
def _get_path(self):
|
||||
return self._path
|
||||
|
||||
def update_headers(self, request_headers):
|
||||
"""
|
||||
Update the session headers with the request ones while ignoring
|
||||
certain name prefixes.
|
||||
|
||||
:type request_headers: dict
|
||||
|
||||
"""
|
||||
for name, value in request_headers.items():
|
||||
|
||||
if name == 'User-Agent' and value.startswith('HTTPie/'):
|
||||
continue
|
||||
|
||||
for prefix in SESSION_IGNORED_HEADER_PREFIXES:
|
||||
if name.lower().startswith(prefix.lower()):
|
||||
break
|
||||
else:
|
||||
self['headers'][name] = value
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return '%s %s %s' % (self.host.name, self.name, self.path)
|
||||
def headers(self):
|
||||
return self['headers']
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
@ -149,6 +123,9 @@ class Session(BaseConfigDict):
|
||||
|
||||
@cookies.setter
|
||||
def cookies(self, jar):
|
||||
"""
|
||||
:type jar: CookieJar
|
||||
"""
|
||||
# http://docs.python.org/2/library/cookielib.html#cookie-objects
|
||||
stored_attrs = ['value', 'path', 'secure', 'expires']
|
||||
self['cookies'] = {}
|
||||
|
@ -1,3 +1,3 @@
|
||||
tox
|
||||
httpbin
|
||||
git+git://github.com/kennethreitz/httpbin.git@7c96875e87a448f08fb1981e85eb79e77d592d98
|
||||
docutils
|
||||
|
2
setup.py
2
setup.py
@ -12,7 +12,7 @@ if sys.argv[-1] == 'test':
|
||||
|
||||
|
||||
requirements = [
|
||||
'requests>=1.0.4',
|
||||
'requests>=1.2.3',
|
||||
'Pygments>=1.5'
|
||||
]
|
||||
if sys.version_info[:2] in ((2, 6), (3, 1)):
|
||||
|
@ -1170,6 +1170,8 @@ class ItemParsingTest(BaseTestCase):
|
||||
# files
|
||||
self.key_value_type('bar\\@baz@%s' % FILE_PATH_ARG)
|
||||
])
|
||||
# `requests.structures.CaseInsensitiveDict` => `dict`
|
||||
headers = dict(headers._store.values())
|
||||
self.assertDictEqual(headers, {
|
||||
'foo:bar': 'baz',
|
||||
'jack@jill': 'hill',
|
||||
@ -1199,6 +1201,8 @@ class ItemParsingTest(BaseTestCase):
|
||||
self.key_value_type('test-file@%s' % FILE_PATH_ARG),
|
||||
self.key_value_type('query==value'),
|
||||
])
|
||||
# `requests.structures.CaseInsensitiveDict` => `dict`
|
||||
headers = dict(headers._store.values())
|
||||
self.assertDictEqual(headers, {
|
||||
'header': 'value',
|
||||
'eh': ''
|
||||
@ -1370,6 +1374,26 @@ class SessionTest(BaseTestCase):
|
||||
self.assertEqual(r.json['headers']['Cookie'], 'hello=world')
|
||||
self.assertIn('Basic ', r.json['headers']['Authorization'])
|
||||
|
||||
def test_session_ignored_header_prefixes(self):
|
||||
r = http(
|
||||
'--session=test',
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
'Content-Type: text/plain',
|
||||
'If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT',
|
||||
env=self.env
|
||||
)
|
||||
self.assertIn(OK, r)
|
||||
|
||||
r2 = http(
|
||||
'--session=test',
|
||||
'GET',
|
||||
httpbin('/get')
|
||||
)
|
||||
self.assertIn(OK, r2)
|
||||
self.assertNotIn('Content-Type', r2.json['headers'])
|
||||
self.assertNotIn('If-Unmodified-Since', r2.json['headers'])
|
||||
|
||||
def test_session_update(self):
|
||||
# Get a response to a request from the original session.
|
||||
r1 = http(
|
||||
@ -1444,6 +1468,28 @@ class SessionTest(BaseTestCase):
|
||||
# Should be the same as before r2.
|
||||
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):
|
||||
|
||||
@ -1559,9 +1605,23 @@ class DownloadTest(BaseTestCase):
|
||||
self.assertIn('Done', r.stderr)
|
||||
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):
|
||||
download = Download(output_file=open(os.devnull, 'w'))
|
||||
download.start(Response(url=httpbin('/')))
|
||||
time.sleep(1.1)
|
||||
download._chunk_downloaded(b'12345')
|
||||
download.finish()
|
||||
self.assertFalse(download.interrupted)
|
||||
|
Reference in New Issue
Block a user