Compare commits

...

17 Commits
0.5.0 ... 0.6.0

Author SHA1 Message Date
809a461a26 v0.6.0 2013-06-03 12:19:43 +02:00
c3d550e930 Fixed headers tests; Require requests>=1.2.3. 2013-06-02 20:47:29 +02:00
172df162b3 Added XML formatting to CHANGELOG. 2013-06-02 20:27:58 +02:00
1bad62ab0e Handle unicode when formatting XML. 2013-06-02 20:25:36 +02:00
8d302f91f9 Merge branch 'master' of git://github.com/jargonjustin/httpie into jargonjustin-master 2013-06-02 20:14:51 +02:00
63b61bc811 Add custom Host example. 2013-05-20 15:31:02 +02:00
5af88756a6 Fixed download ETA for Python 2.6. 2013-05-14 12:49:29 +02:00
7f624e61b5 Use Thread instead of Timer for progress reporting. 2013-05-14 12:49:03 +02:00
6e848b3203 cleanup 2013-05-14 12:14:08 +02:00
8e112a6948 test_download_no_Content_Length 2013-05-13 15:35:12 +02:00
87c59ae561 Added anonymous sessions (--session=/file/path.json). 2013-05-13 14:47:44 +02:00
76eebeac2a 0.6.0-dev 2013-05-13 12:42:16 +02:00
5b9cbcb530 v0.5.1 2013-05-13 12:40:25 +02:00
8ad33d5f6a Changelog 2013-05-13 12:20:54 +02:00
86ac4cdb7b Changelog 2013-05-13 12:20:28 +02:00
e09b74021c Ignore Content-* and If-* request headers.
Those headers are not stored in sessions anymore.

Closes #141.
2013-05-13 11:54:49 +02:00
2e57c080fd Pretty print XML 2012-12-17 13:21:38 -08:00
13 changed files with 263 additions and 157 deletions

View File

@ -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>`_

View File

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

View File

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

View File

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

View File

@ -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),
)

View File

@ -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 = {

View File

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

View File

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

View File

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

View File

@ -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'] = {}

View File

@ -1,3 +1,3 @@
tox
httpbin
git+git://github.com/kennethreitz/httpbin.git@7c96875e87a448f08fb1981e85eb79e77d592d98
docutils

View File

@ -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)):

View File

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