mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-10 01:25:34 +00:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a351621d92 | ||
|
|
f387950486 | ||
|
|
8529c46f00 | ||
|
|
5afa8f703a | ||
|
|
641d9e5d49 | ||
|
|
0ef400195b | ||
|
|
133423ce94 | ||
|
|
7d2d29de12 | ||
|
|
023e41bb4c | ||
|
|
6ae93ac820 | ||
|
|
04b7f4fc65 | ||
|
|
936feb7748 | ||
|
|
79d26ebb43 | ||
|
|
b8024de1b8 | ||
|
|
2f94d06e9b | ||
|
|
042ee790e2 | ||
|
|
a249781b97 | ||
|
|
b64e93aff2 | ||
|
|
4897a8e692 | ||
|
|
15d79e5b78 | ||
|
|
1b9f80d741 | ||
|
|
20fb283e97 | ||
|
|
e868b64922 | ||
|
|
69cecdbda7 | ||
|
|
be53091ae5 | ||
|
|
ba91053485 | ||
|
|
837992767e | ||
|
|
cd0907ffaf | ||
|
|
77d838e0fc | ||
|
|
5362db2ebb | ||
|
|
f9ce14d29a | ||
|
|
5242e68cd1 | ||
|
|
9817a8bda5 | ||
|
|
6e1768b85b | ||
|
|
062126e50c | ||
|
|
438550959f | ||
|
|
69e4316545 | ||
|
|
2f53776ffb | ||
|
|
535efe1eb9 | ||
|
|
eb2e226bb8 | ||
|
|
8fe2ab6d06 | ||
|
|
6ac535f18d | ||
|
|
bceaab8b88 | ||
|
|
0c2bbe0d51 | ||
|
|
2e5fdd36d5 | ||
|
|
f8b9a41f13 | ||
|
|
6e040030b8 |
@@ -12,6 +12,7 @@ env:
|
|||||||
- TOX_SUFFIX="urllib3"
|
- TOX_SUFFIX="urllib3"
|
||||||
- TOX_SUFFIX="tornado4"
|
- TOX_SUFFIX="tornado4"
|
||||||
- TOX_SUFFIX="aiohttp"
|
- TOX_SUFFIX="aiohttp"
|
||||||
|
- TOX_SUFFIX="httpx"
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Only run lint on a single 3.x
|
# Only run lint on a single 3.x
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ VCR.py will detect the absence of a cassette file and once again record
|
|||||||
all HTTP interactions, which will update them to correspond to the new
|
all HTTP interactions, which will update them to correspond to the new
|
||||||
API.
|
API.
|
||||||
|
|
||||||
|
Usage with Pytest
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
There is a library to provide some pytest fixtures called pytest-recording https://github.com/kiwicom/pytest-recording
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ consider part of the API. The fields are as follows:
|
|||||||
been played back.
|
been played back.
|
||||||
- ``responses_of(request)``: Access the responses that match a given
|
- ``responses_of(request)``: Access the responses that match a given
|
||||||
request
|
request
|
||||||
|
- ``allow_playback_repeats``: A boolean indicating whether responses
|
||||||
|
can be played back more than once.
|
||||||
|
|
||||||
The ``Request`` object has the following properties:
|
The ``Request`` object has the following properties:
|
||||||
|
|
||||||
@@ -386,3 +388,19 @@ VCR.py allows to rewind a cassette in order to replay it inside the same functio
|
|||||||
assert cass.all_played
|
assert cass.all_played
|
||||||
cass.rewind()
|
cass.rewind()
|
||||||
assert not cass.all_played
|
assert not cass.all_played
|
||||||
|
|
||||||
|
Playback Repeats
|
||||||
|
----------------
|
||||||
|
|
||||||
|
By default, each response in a cassette can only be matched and played back
|
||||||
|
once while the cassette is in use, unless the cassette is rewound.
|
||||||
|
|
||||||
|
If you want to allow playback repeats without rewinding the cassette, use
|
||||||
|
the Cassette ``allow_playback_repeats`` option.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml', allow_playback_repeats=True) as cass:
|
||||||
|
for x in range(10):
|
||||||
|
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||||
|
assert cass.all_played
|
||||||
|
|||||||
@@ -7,8 +7,13 @@ For a full list of triaged issues, bugs and PRs and what release they are target
|
|||||||
|
|
||||||
All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out.
|
All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out.
|
||||||
|
|
||||||
- UNRELEASED
|
- 4.1.0
|
||||||
- ...
|
- Add support for httpx!! (thanks @herdigiorgi)
|
||||||
|
- Add the new `allow_playback_repeats` option (thanks @tysonholub)
|
||||||
|
- Several aiohttp improvements (cookie support, multiple headers with same key) (Thanks @pauloromeira)
|
||||||
|
- Use enums for record modes (thanks @aaronbannin)
|
||||||
|
- Bugfix: Do not redirect on 304 in aiohttp (Thanks @royjs)
|
||||||
|
- Bugfix: Fix test suite by switching to mockbin (thanks @jairhenrique)
|
||||||
- 4.0.2
|
- 4.0.2
|
||||||
- Fix mock imports as reported in #504 by @llybin. Thank you.
|
- Fix mock imports as reported in #504 by @llybin. Thank you.
|
||||||
- 4.0.1
|
- 4.0.1
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ The following HTTP libraries are supported:
|
|||||||
- ``tornado.httpclient``
|
- ``tornado.httpclient``
|
||||||
- ``urllib2``
|
- ``urllib2``
|
||||||
- ``urllib3``
|
- ``urllib3``
|
||||||
|
- ``httpx``
|
||||||
|
|
||||||
Speed
|
Speed
|
||||||
-----
|
-----
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: ''
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- '*/*'
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, br
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
host:
|
||||||
|
- httpbin.org
|
||||||
|
user-agent:
|
||||||
|
- python-httpx/0.12.1
|
||||||
|
method: GET
|
||||||
|
uri: https://httpbin.org/headers
|
||||||
|
response:
|
||||||
|
content: "{\n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\"\
|
||||||
|
: \"gzip, deflate, br\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\"\
|
||||||
|
: \"python-httpx/0.12.1\", \n \"X-Amzn-Trace-Id\": \"Root=1-5ea778c9-ea76170da792abdbf7614067\"\
|
||||||
|
\n }\n}\n"
|
||||||
|
headers:
|
||||||
|
access-control-allow-credentials:
|
||||||
|
- 'true'
|
||||||
|
access-control-allow-origin:
|
||||||
|
- '*'
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '226'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
date:
|
||||||
|
- Tue, 28 Apr 2020 00:28:57 GMT
|
||||||
|
server:
|
||||||
|
- gunicorn/19.9.0
|
||||||
|
via:
|
||||||
|
- my_own_proxy
|
||||||
|
http_version: HTTP/1.1
|
||||||
|
status_code: 200
|
||||||
|
version: 1
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -179,9 +180,8 @@ def test_params_same_url_distinct_params(tmpdir, scheme):
|
|||||||
|
|
||||||
other_params = {"other": "params"}
|
other_params = {"other": "params"}
|
||||||
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
response, cassette_response_text = get(url, output="text", params=other_params)
|
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||||
assert "No match for the request" in cassette_response_text
|
get(url, output="text", params=other_params)
|
||||||
assert response.status == 599
|
|
||||||
|
|
||||||
|
|
||||||
def test_params_on_url(tmpdir, scheme):
|
def test_params_on_url(tmpdir, scheme):
|
||||||
@@ -250,7 +250,7 @@ def test_aiohttp_test_client_json(aiohttp_client, tmpdir):
|
|||||||
|
|
||||||
|
|
||||||
def test_redirect(aiohttp_client, tmpdir):
|
def test_redirect(aiohttp_client, tmpdir):
|
||||||
url = "https://httpbin.org/redirect/2"
|
url = "https://mockbin.org/redirect/302/2"
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("redirect.yaml"))):
|
||||||
response, _ = get(url)
|
response, _ = get(url)
|
||||||
@@ -273,6 +273,23 @@ def test_redirect(aiohttp_client, tmpdir):
|
|||||||
assert cassette_response.request_info.real_url == response.request_info.real_url
|
assert cassette_response.request_info.real_url == response.request_info.real_url
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_modified(aiohttp_client, tmpdir):
|
||||||
|
"""It doesn't try to redirect on 304"""
|
||||||
|
url = "https://httpbin.org/status/304"
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))):
|
||||||
|
response, _ = get(url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("not_modified.yaml"))) as cassette:
|
||||||
|
cassette_response, _ = get(url)
|
||||||
|
|
||||||
|
assert cassette_response.status == 304
|
||||||
|
assert response.status == 304
|
||||||
|
assert len(cassette_response.history) == len(response.history)
|
||||||
|
assert len(cassette) == 1
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_double_requests(tmpdir):
|
def test_double_requests(tmpdir):
|
||||||
"""We should capture, record, and replay all requests and response chains,
|
"""We should capture, record, and replay all requests and response chains,
|
||||||
even if there are duplicate ones.
|
even if there are duplicate ones.
|
||||||
@@ -302,3 +319,83 @@ def test_double_requests(tmpdir):
|
|||||||
|
|
||||||
# Now that we made both requests, we should have played both.
|
# Now that we made both requests, we should have played both.
|
||||||
assert cassette.play_count == 2
|
assert cassette.play_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_cookies(scheme, tmpdir):
|
||||||
|
async def run(loop):
|
||||||
|
cookies_url = scheme + (
|
||||||
|
"://httpbin.org/response-headers?"
|
||||||
|
"set-cookie=" + urllib.parse.quote("cookie_1=val_1; Path=/") + "&"
|
||||||
|
"Set-Cookie=" + urllib.parse.quote("Cookie_2=Val_2; Path=/")
|
||||||
|
)
|
||||||
|
home_url = scheme + "://httpbin.org/"
|
||||||
|
tmp = str(tmpdir.join("cookies.yaml"))
|
||||||
|
req_cookies = {"Cookie_3": "Val_3"}
|
||||||
|
req_headers = {"Cookie": "Cookie_4=Val_4"}
|
||||||
|
|
||||||
|
# ------------------------- Record -------------------------- #
|
||||||
|
with vcr.use_cassette(tmp) as cassette:
|
||||||
|
async with aiohttp.ClientSession(loop=loop) as session:
|
||||||
|
cookies_resp = await session.get(cookies_url)
|
||||||
|
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
|
||||||
|
assert cassette.play_count == 0
|
||||||
|
assert_responses(cookies_resp, home_resp)
|
||||||
|
|
||||||
|
# -------------------------- Play --------------------------- #
|
||||||
|
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||||
|
async with aiohttp.ClientSession(loop=loop) as session:
|
||||||
|
cookies_resp = await session.get(cookies_url)
|
||||||
|
home_resp = await session.get(home_url, cookies=req_cookies, headers=req_headers)
|
||||||
|
assert cassette.play_count == 2
|
||||||
|
assert_responses(cookies_resp, home_resp)
|
||||||
|
|
||||||
|
def assert_responses(cookies_resp, home_resp):
|
||||||
|
assert cookies_resp.cookies.get("cookie_1").value == "val_1"
|
||||||
|
assert cookies_resp.cookies.get("Cookie_2").value == "Val_2"
|
||||||
|
request_cookies = home_resp.request_info.headers["cookie"]
|
||||||
|
assert "cookie_1=val_1" in request_cookies
|
||||||
|
assert "Cookie_2=Val_2" in request_cookies
|
||||||
|
assert "Cookie_3=Val_3" in request_cookies
|
||||||
|
assert "Cookie_4=Val_4" in request_cookies
|
||||||
|
|
||||||
|
run_in_loop(run)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cookies_redirect(scheme, tmpdir):
|
||||||
|
async def run(loop):
|
||||||
|
# Sets cookie as provided by the query string and redirects
|
||||||
|
cookies_url = scheme + "://httpbin.org/cookies/set?Cookie_1=Val_1"
|
||||||
|
tmp = str(tmpdir.join("cookies.yaml"))
|
||||||
|
|
||||||
|
# ------------------------- Record -------------------------- #
|
||||||
|
with vcr.use_cassette(tmp) as cassette:
|
||||||
|
async with aiohttp.ClientSession(loop=loop) as session:
|
||||||
|
cookies_resp = await session.get(cookies_url)
|
||||||
|
assert not cookies_resp.cookies
|
||||||
|
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
||||||
|
assert cookies["Cookie_1"].value == "Val_1"
|
||||||
|
assert cassette.play_count == 0
|
||||||
|
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
|
||||||
|
|
||||||
|
# -------------------------- Play --------------------------- #
|
||||||
|
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||||
|
async with aiohttp.ClientSession(loop=loop) as session:
|
||||||
|
cookies_resp = await session.get(cookies_url)
|
||||||
|
assert not cookies_resp.cookies
|
||||||
|
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
||||||
|
assert cookies["Cookie_1"].value == "Val_1"
|
||||||
|
assert cassette.play_count == 2
|
||||||
|
cassette.requests[1].headers["Cookie"] == "Cookie_1=Val_1"
|
||||||
|
|
||||||
|
# Assert that it's ignoring expiration date
|
||||||
|
with vcr.use_cassette(tmp, record_mode=vcr.mode.NONE) as cassette:
|
||||||
|
cassette.responses[0]["headers"]["set-cookie"] = [
|
||||||
|
"Cookie_1=Val_1; Expires=Wed, 21 Oct 2015 07:28:00 GMT"
|
||||||
|
]
|
||||||
|
async with aiohttp.ClientSession(loop=loop) as session:
|
||||||
|
cookies_resp = await session.get(cookies_url)
|
||||||
|
assert not cookies_resp.cookies
|
||||||
|
cookies = session.cookie_jar.filter_cookies(cookies_url)
|
||||||
|
assert cookies["Cookie_1"].value == "Val_1"
|
||||||
|
|
||||||
|
run_in_loop(run)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def test_disk_saver_write(tmpdir, httpbin):
|
|||||||
# the mtime doesn't change
|
# the mtime doesn't change
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
with vcr.use_cassette(fname, record_mode="any") as cass:
|
with vcr.use_cassette(fname, record_mode=vcr.mode.ANY) as cass:
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
urlopen(httpbin.url + "/get").read()
|
urlopen(httpbin.url + "/get").read()
|
||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
|
|||||||
255
tests/integration/test_httpx.py
Normal file
255
tests/integration/test_httpx.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import pytest
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
asyncio = pytest.importorskip("asyncio")
|
||||||
|
httpx = pytest.importorskip("httpx")
|
||||||
|
|
||||||
|
import vcr # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDoRequest:
|
||||||
|
_client_class = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._client = self._client_class(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class DoSyncRequest(BaseDoRequest):
|
||||||
|
_client_class = httpx.Client
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return self._client.request(*args, timeout=60, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class DoAsyncRequest(BaseDoRequest):
|
||||||
|
_client_class = httpx.AsyncClient
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def run_in_loop(coroutine):
|
||||||
|
with contextlib.closing(asyncio.new_event_loop()) as loop:
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
task = loop.create_task(coroutine)
|
||||||
|
return loop.run_until_complete(task)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
async def _request():
|
||||||
|
async with self._client as c:
|
||||||
|
return await c.request(*args, **kwargs)
|
||||||
|
|
||||||
|
return DoAsyncRequest.run_in_loop(_request())
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_generate_tests(metafunc):
|
||||||
|
if "do_request" in metafunc.fixturenames:
|
||||||
|
metafunc.parametrize("do_request", [DoAsyncRequest, DoSyncRequest])
|
||||||
|
if "scheme" in metafunc.fixturenames:
|
||||||
|
metafunc.parametrize("scheme", ["http", "https"])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def yml(tmpdir, request):
|
||||||
|
return str(tmpdir.join(request.function.__name__ + ".yaml"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_status(tmpdir, scheme, do_request):
|
||||||
|
url = scheme + "://mockbin.org/request"
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("status.yaml"))):
|
||||||
|
response = do_request()("GET", url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("status.yaml"))) as cassette:
|
||||||
|
cassette_response = do_request()("GET", url)
|
||||||
|
assert cassette_response.status_code == response.status_code
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_case_insensitive_headers(tmpdir, scheme, do_request):
|
||||||
|
url = scheme + "://mockbin.org/request"
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))):
|
||||||
|
do_request()("GET", url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("whatever.yaml"))) as cassette:
|
||||||
|
cassette_response = do_request()("GET", url)
|
||||||
|
assert "Content-Type" in cassette_response.headers
|
||||||
|
assert "content-type" in cassette_response.headers
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_content(tmpdir, scheme, do_request):
|
||||||
|
url = scheme + "://httpbin.org"
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))):
|
||||||
|
response = do_request()("GET", url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("cointent.yaml"))) as cassette:
|
||||||
|
cassette_response = do_request()("GET", url)
|
||||||
|
assert cassette_response.content == response.content
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_json(tmpdir, scheme, do_request):
|
||||||
|
url = scheme + "://httpbin.org/get"
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("json.yaml"))):
|
||||||
|
response = do_request(headers=headers)("GET", url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("json.yaml"))) as cassette:
|
||||||
|
cassette_response = do_request(headers=headers)("GET", url)
|
||||||
|
assert cassette_response.json() == response.json()
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_params_same_url_distinct_params(tmpdir, scheme, do_request):
|
||||||
|
url = scheme + "://httpbin.org/get"
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
params = {"a": 1, "b": False, "c": "c"}
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
|
response = do_request()("GET", url, params=params, headers=headers)
|
||||||
|
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
|
cassette_response = do_request()("GET", url, params=params, headers=headers)
|
||||||
|
assert cassette_response.request.url == response.request.url
|
||||||
|
assert cassette_response.json() == response.json()
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
params = {"other": "params"}
|
||||||
|
with vcr.use_cassette(str(tmpdir.join("get.yaml"))) as cassette:
|
||||||
|
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||||
|
do_request()("GET", url, params=params, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect(tmpdir, do_request, yml):
|
||||||
|
url = "https://mockbin.org/redirect/303/2"
|
||||||
|
|
||||||
|
response = do_request()("GET", url)
|
||||||
|
with vcr.use_cassette(yml):
|
||||||
|
response = do_request()("GET", url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(yml) as cassette:
|
||||||
|
cassette_response = do_request()("GET", url)
|
||||||
|
|
||||||
|
assert cassette_response.status_code == response.status_code
|
||||||
|
assert len(cassette_response.history) == len(response.history)
|
||||||
|
assert len(cassette) == 3
|
||||||
|
assert cassette.play_count == 3
|
||||||
|
|
||||||
|
# Assert that the real response and the cassette response have a similar
|
||||||
|
# looking request_info.
|
||||||
|
assert cassette_response.request.url == response.request.url
|
||||||
|
assert cassette_response.request.method == response.request.method
|
||||||
|
assert {k: v for k, v in cassette_response.request.headers.items()} == {
|
||||||
|
k: v for k, v in response.request.headers.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_work_with_gzipped_data(tmpdir, do_request, yml):
|
||||||
|
with vcr.use_cassette(yml):
|
||||||
|
do_request()("GET", "https://httpbin.org/gzip")
|
||||||
|
|
||||||
|
with vcr.use_cassette(yml) as cassette:
|
||||||
|
cassette_response = do_request()("GET", "https://httpbin.org/gzip")
|
||||||
|
|
||||||
|
assert "gzip" in cassette_response.json()["headers"]["Accept-Encoding"]
|
||||||
|
assert cassette_response.read()
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url", ["https://github.com/kevin1024/vcrpy/issues/" + str(i) for i in range(3, 6)])
|
||||||
|
def test_simple_fetching(tmpdir, do_request, yml, url):
|
||||||
|
with vcr.use_cassette(yml):
|
||||||
|
do_request()("GET", url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(yml) as cassette:
|
||||||
|
cassette_response = do_request()("GET", url)
|
||||||
|
assert str(cassette_response.request.url) == url
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_behind_proxy(do_request):
|
||||||
|
# This is recorded because otherwise we should have a live proxy somewhere.
|
||||||
|
yml = (
|
||||||
|
os.path.dirname(os.path.realpath(__file__)) + "/cassettes/" + "test_httpx_test_test_behind_proxy.yml"
|
||||||
|
)
|
||||||
|
url = "https://httpbin.org/headers"
|
||||||
|
proxy = "http://localhost:8080"
|
||||||
|
proxies = {"http": proxy, "https": proxy}
|
||||||
|
|
||||||
|
with vcr.use_cassette(yml):
|
||||||
|
response = do_request(proxies=proxies, verify=False)("GET", url)
|
||||||
|
|
||||||
|
with vcr.use_cassette(yml) as cassette:
|
||||||
|
cassette_response = do_request(proxies=proxies, verify=False)("GET", url)
|
||||||
|
assert str(cassette_response.request.url) == url
|
||||||
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
assert cassette_response.headers["Via"] == "my_own_proxy", str(cassette_response.headers)
|
||||||
|
assert cassette_response.request.url == response.request.url
|
||||||
|
|
||||||
|
|
||||||
|
def test_cookies(tmpdir, scheme, do_request):
|
||||||
|
def client_cookies(client):
|
||||||
|
return [c for c in client._client.cookies]
|
||||||
|
|
||||||
|
def response_cookies(response):
|
||||||
|
return [c for c in response.cookies]
|
||||||
|
|
||||||
|
client = do_request()
|
||||||
|
assert client_cookies(client) == []
|
||||||
|
|
||||||
|
url = scheme + "://httpbin.org"
|
||||||
|
testfile = str(tmpdir.join("cookies.yml"))
|
||||||
|
with vcr.use_cassette(testfile):
|
||||||
|
r1 = client("GET", url + "/cookies/set?k1=v1&k2=v2")
|
||||||
|
assert response_cookies(r1.history[0]) == ["k1", "k2"]
|
||||||
|
assert response_cookies(r1) == []
|
||||||
|
|
||||||
|
r2 = client("GET", url + "/cookies")
|
||||||
|
assert len(r2.json()["cookies"]) == 2
|
||||||
|
|
||||||
|
assert client_cookies(client) == ["k1", "k2"]
|
||||||
|
|
||||||
|
new_client = do_request()
|
||||||
|
assert client_cookies(new_client) == []
|
||||||
|
|
||||||
|
with vcr.use_cassette(testfile) as cassette:
|
||||||
|
cassette_response = new_client("GET", url + "/cookies/set?k1=v1&k2=v2")
|
||||||
|
assert response_cookies(cassette_response.history[0]) == ["k1", "k2"]
|
||||||
|
assert response_cookies(cassette_response) == []
|
||||||
|
|
||||||
|
assert cassette.play_count == 2
|
||||||
|
assert client_cookies(new_client) == ["k1", "k2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_relative_redirects(tmpdir, scheme, do_request):
|
||||||
|
url = scheme + "://mockbin.com/redirect/301?to=/redirect/301?to=/request"
|
||||||
|
testfile = str(tmpdir.join("relative_redirects.yml"))
|
||||||
|
with vcr.use_cassette(testfile):
|
||||||
|
response = do_request()("GET", url)
|
||||||
|
assert len(response.history) == 2, response
|
||||||
|
assert response.json()["url"].endswith("request")
|
||||||
|
|
||||||
|
with vcr.use_cassette(testfile) as cassette:
|
||||||
|
response = do_request()("GET", url)
|
||||||
|
assert len(response.history) == 2
|
||||||
|
assert response.json()["url"].endswith("request")
|
||||||
|
|
||||||
|
assert cassette.play_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect_wo_allow_redirects(do_request, yml):
|
||||||
|
url = "https://mockbin.org/redirect/308/5"
|
||||||
|
|
||||||
|
with vcr.use_cassette(yml):
|
||||||
|
response = do_request()("GET", url, allow_redirects=False)
|
||||||
|
|
||||||
|
assert str(response.url).endswith("308/5")
|
||||||
|
assert response.status_code == 308
|
||||||
|
|
||||||
|
with vcr.use_cassette(yml) as cassette:
|
||||||
|
response = do_request()("GET", url, allow_redirects=False)
|
||||||
|
|
||||||
|
assert str(response.url).endswith("308/5")
|
||||||
|
assert response.status_code == 308
|
||||||
|
|
||||||
|
assert cassette.play_count == 1
|
||||||
@@ -13,13 +13,13 @@ def _replace_httpbin(uri, httpbin, httpbin_secure):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cassette(tmpdir, httpbin, httpbin_secure):
|
def cassette(tmpdir, httpbin, httpbin_secure):
|
||||||
"""
|
"""
|
||||||
Helper fixture used to prepare the cassete
|
Helper fixture used to prepare the cassette
|
||||||
returns path to the recorded cassette
|
returns path to the recorded cassette
|
||||||
"""
|
"""
|
||||||
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
|
default_uri = _replace_httpbin(DEFAULT_URI, httpbin, httpbin_secure)
|
||||||
|
|
||||||
cassette_path = str(tmpdir.join("test.yml"))
|
cassette_path = str(tmpdir.join("test.yml"))
|
||||||
with vcr.use_cassette(cassette_path, record_mode="all"):
|
with vcr.use_cassette(cassette_path, record_mode=vcr.mode.ALL):
|
||||||
urlopen(default_uri)
|
urlopen(default_uri)
|
||||||
return cassette_path
|
return cassette_path
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ from urllib.request import urlopen
|
|||||||
|
|
||||||
def test_once_record_mode(tmpdir, httpbin):
|
def test_once_record_mode(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# cassette file doesn't exist, so create.
|
# cassette file doesn't exist, so create.
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# make the same request again
|
# make the same request again
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
@@ -22,12 +22,12 @@ def test_once_record_mode(tmpdir, httpbin):
|
|||||||
|
|
||||||
def test_once_record_mode_two_times(tmpdir, httpbin):
|
def test_once_record_mode_two_times(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# get two of the same file
|
# get two of the same file
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# do it again
|
# do it again
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
@@ -35,7 +35,7 @@ def test_once_record_mode_two_times(tmpdir, httpbin):
|
|||||||
|
|
||||||
def test_once_mode_three_times(tmpdir, httpbin):
|
def test_once_mode_three_times(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# get three of the same file
|
# get three of the same file
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
@@ -45,11 +45,11 @@ def test_once_mode_three_times(tmpdir, httpbin):
|
|||||||
def test_new_episodes_record_mode(tmpdir, httpbin):
|
def test_new_episodes_record_mode(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="new_episodes"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
|
||||||
# cassette file doesn't exist, so create.
|
# cassette file doesn't exist, so create.
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
|
||||||
# make the same request again
|
# make the same request again
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
|
|||||||
# not all responses have been played
|
# not all responses have been played
|
||||||
assert not cass.all_played
|
assert not cass.all_played
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES) as cass:
|
||||||
# the cassette should now have 2 responses
|
# the cassette should now have 2 responses
|
||||||
assert len(cass.responses) == 2
|
assert len(cass.responses) == 2
|
||||||
|
|
||||||
@@ -74,11 +74,11 @@ def test_new_episodes_record_mode(tmpdir, httpbin):
|
|||||||
def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
url = httpbin.url + "/bytes/1024"
|
url = httpbin.url + "/bytes/1024"
|
||||||
with vcr.use_cassette(testfile, record_mode="new_episodes"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
|
||||||
# cassette file doesn't exist, so create.
|
# cassette file doesn't exist, so create.
|
||||||
original_first_response = urlopen(url).read()
|
original_first_response = urlopen(url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="new_episodes"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NEW_EPISODES):
|
||||||
# make the same request again
|
# make the same request again
|
||||||
assert urlopen(url).read() == original_first_response
|
assert urlopen(url).read() == original_first_response
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
|||||||
# to the cassette without repercussions
|
# to the cassette without repercussions
|
||||||
original_second_response = urlopen(url).read()
|
original_second_response = urlopen(url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="once"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ONCE):
|
||||||
# make the same request again
|
# make the same request again
|
||||||
assert urlopen(url).read() == original_first_response
|
assert urlopen(url).read() == original_first_response
|
||||||
assert urlopen(url).read() == original_second_response
|
assert urlopen(url).read() == original_second_response
|
||||||
@@ -99,11 +99,11 @@ def test_new_episodes_record_mode_two_times(tmpdir, httpbin):
|
|||||||
def test_all_record_mode(tmpdir, httpbin):
|
def test_all_record_mode(tmpdir, httpbin):
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="all"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL):
|
||||||
# cassette file doesn't exist, so create.
|
# cassette file doesn't exist, so create.
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="all") as cass:
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL) as cass:
|
||||||
# make the same request again
|
# make the same request again
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ def test_none_record_mode(tmpdir, httpbin):
|
|||||||
# Cassette file doesn't exist, yet we are trying to make a request.
|
# Cassette file doesn't exist, yet we are trying to make a request.
|
||||||
# raise hell.
|
# raise hell.
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
with vcr.use_cassette(testfile, record_mode="none"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE):
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
@@ -130,11 +130,11 @@ def test_none_record_mode_with_existing_cassette(tmpdir, httpbin):
|
|||||||
# create a cassette file
|
# create a cassette file
|
||||||
testfile = str(tmpdir.join("recordmode.yml"))
|
testfile = str(tmpdir.join("recordmode.yml"))
|
||||||
|
|
||||||
with vcr.use_cassette(testfile, record_mode="all"):
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.ALL):
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
|
|
||||||
# play from cassette file
|
# play from cassette file
|
||||||
with vcr.use_cassette(testfile, record_mode="none") as cass:
|
with vcr.use_cassette(testfile, record_mode=vcr.mode.NONE) as cass:
|
||||||
urlopen(httpbin.url).read()
|
urlopen(httpbin.url).read()
|
||||||
assert cass.play_count == 1
|
assert cass.play_count == 1
|
||||||
# but if I try to hit the net, raise an exception.
|
# but if I try to hit the net, raise an exception.
|
||||||
|
|||||||
@@ -137,6 +137,31 @@ def test_cassette_all_played():
|
|||||||
assert a.all_played
|
assert a.all_played
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
|
||||||
|
def test_cassette_allow_playback_repeats():
|
||||||
|
a = Cassette("test", allow_playback_repeats=True)
|
||||||
|
a.append("foo", "bar")
|
||||||
|
a.append("other", "resp")
|
||||||
|
for x in range(10):
|
||||||
|
assert a.play_response("foo") == "bar"
|
||||||
|
assert a.play_count == 10
|
||||||
|
assert a.all_played is False
|
||||||
|
assert a.play_response("other") == "resp"
|
||||||
|
assert a.play_count == 11
|
||||||
|
assert a.all_played
|
||||||
|
|
||||||
|
a.allow_playback_repeats = False
|
||||||
|
with pytest.raises(UnhandledHTTPRequestError) as e:
|
||||||
|
a.play_response("foo")
|
||||||
|
assert str(e.value) == "\"The cassette ('test') doesn't contain the request ('foo') asked for\""
|
||||||
|
a.rewind()
|
||||||
|
assert a.all_played is False
|
||||||
|
assert a.play_response("foo") == "bar"
|
||||||
|
assert a.all_played is False
|
||||||
|
assert a.play_response("other") == "resp"
|
||||||
|
assert a.all_played
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
|
@mock.patch("vcr.cassette.requests_match", _mock_requests_match)
|
||||||
def test_cassette_rewound():
|
def test_cassette_rewound():
|
||||||
a = Cassette("test")
|
a = Cassette("test")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from vcr import mode
|
||||||
from vcr.stubs import VCRHTTPSConnection
|
from vcr.stubs import VCRHTTPSConnection
|
||||||
from vcr.cassette import Cassette
|
from vcr.cassette import Cassette
|
||||||
|
|
||||||
@@ -13,6 +14,6 @@ class TestVCRConnection:
|
|||||||
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
|
@mock.patch("vcr.cassette.Cassette.can_play_response_for", return_value=False)
|
||||||
def testing_connect(*args):
|
def testing_connect(*args):
|
||||||
vcr_connection = VCRHTTPSConnection("www.google.com")
|
vcr_connection = VCRHTTPSConnection("www.google.com")
|
||||||
vcr_connection.cassette = Cassette("test", record_mode="all")
|
vcr_connection.cassette = Cassette("test", record_mode=mode.ALL)
|
||||||
vcr_connection.real_connection.connect()
|
vcr_connection.real_connection.connect()
|
||||||
assert vcr_connection.real_connection.sock is not None
|
assert vcr_connection.real_connection.sock is not None
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
import http.client as httplib
|
import http.client as httplib
|
||||||
|
|
||||||
from vcr import VCR, use_cassette
|
from vcr import VCR, mode, use_cassette
|
||||||
from vcr.request import Request
|
from vcr.request import Request
|
||||||
from vcr.stubs import VCRHTTPSConnection
|
from vcr.stubs import VCRHTTPSConnection
|
||||||
from vcr.patch import _HTTPConnection, force_reset
|
from vcr.patch import _HTTPConnection, force_reset
|
||||||
@@ -188,11 +188,11 @@ def test_custom_patchers():
|
|||||||
def test_inject_cassette():
|
def test_inject_cassette():
|
||||||
vcr = VCR(inject_cassette=True)
|
vcr = VCR(inject_cassette=True)
|
||||||
|
|
||||||
@vcr.use_cassette("test", record_mode="once")
|
@vcr.use_cassette("test", record_mode=mode.ONCE)
|
||||||
def with_cassette_injected(cassette):
|
def with_cassette_injected(cassette):
|
||||||
assert cassette.record_mode == "once"
|
assert cassette.record_mode == mode.ONCE
|
||||||
|
|
||||||
@vcr.use_cassette("test", record_mode="once", inject_cassette=False)
|
@vcr.use_cassette("test", record_mode=mode.ONCE, inject_cassette=False)
|
||||||
def without_cassette_injected():
|
def without_cassette_injected():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ def test_inject_cassette():
|
|||||||
|
|
||||||
|
|
||||||
def test_with_current_defaults():
|
def test_with_current_defaults():
|
||||||
vcr = VCR(inject_cassette=True, record_mode="once")
|
vcr = VCR(inject_cassette=True, record_mode=mode.ONCE)
|
||||||
|
|
||||||
@vcr.use_cassette("test", with_current_defaults=False)
|
@vcr.use_cassette("test", with_current_defaults=False)
|
||||||
def changing_defaults(cassette, checks):
|
def changing_defaults(cassette, checks):
|
||||||
@@ -212,10 +212,10 @@ def test_with_current_defaults():
|
|||||||
checks(cassette)
|
checks(cassette)
|
||||||
|
|
||||||
def assert_record_mode_once(cassette):
|
def assert_record_mode_once(cassette):
|
||||||
assert cassette.record_mode == "once"
|
assert cassette.record_mode == mode.ONCE
|
||||||
|
|
||||||
def assert_record_mode_all(cassette):
|
def assert_record_mode_all(cassette):
|
||||||
assert cassette.record_mode == "all"
|
assert cassette.record_mode == mode.ALL
|
||||||
|
|
||||||
changing_defaults(assert_record_mode_once)
|
changing_defaults(assert_record_mode_once)
|
||||||
current_defaults(assert_record_mode_once)
|
current_defaults(assert_record_mode_once)
|
||||||
|
|||||||
5
tox.ini
5
tox.ini
@@ -4,6 +4,7 @@ envlist =
|
|||||||
cov-clean,
|
cov-clean,
|
||||||
lint,
|
lint,
|
||||||
{py35,py36,py37,py38}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp},
|
{py35,py36,py37,py38}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp},
|
||||||
|
{py36,py37,py38}-{httpx}
|
||||||
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
|
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
|
||||||
cov-report
|
cov-report
|
||||||
|
|
||||||
@@ -79,8 +80,10 @@ deps =
|
|||||||
aiohttp: aiohttp
|
aiohttp: aiohttp
|
||||||
aiohttp: pytest-asyncio
|
aiohttp: pytest-asyncio
|
||||||
aiohttp: pytest-aiohttp
|
aiohttp: pytest-aiohttp
|
||||||
|
{py36,py37,py38}-{httpx}: httpx
|
||||||
|
{py36,py37,py38}-{httpx}: pytest-asyncio
|
||||||
depends =
|
depends =
|
||||||
lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}: cov-clean
|
lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp},{py36,py37,py38}-{httpx}: cov-clean
|
||||||
cov-report: lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}
|
cov-report: lint,{py35,py36,py37,py38,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py35,py36,py37,py38}-{aiohttp}
|
||||||
passenv =
|
passenv =
|
||||||
AWS_ACCESS_KEY_ID
|
AWS_ACCESS_KEY_ID
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
from .config import VCR
|
from .config import VCR
|
||||||
from logging import NullHandler
|
from logging import NullHandler
|
||||||
|
from .record_mode import RecordMode as mode # noqa import is not used in this file
|
||||||
|
|
||||||
__version__ = "4.0.2"
|
__version__ = "4.1.0"
|
||||||
|
|
||||||
logging.getLogger(__name__).addHandler(NullHandler())
|
logging.getLogger(__name__).addHandler(NullHandler())
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from .serializers import yamlserializer
|
|||||||
from .persisters.filesystem import FilesystemPersister
|
from .persisters.filesystem import FilesystemPersister
|
||||||
from .util import partition_dict
|
from .util import partition_dict
|
||||||
from ._handle_coroutine import handle_coroutine
|
from ._handle_coroutine import handle_coroutine
|
||||||
|
from .record_mode import RecordMode
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from asyncio import iscoroutinefunction
|
from asyncio import iscoroutinefunction
|
||||||
@@ -175,12 +176,13 @@ class Cassette:
|
|||||||
path,
|
path,
|
||||||
serializer=None,
|
serializer=None,
|
||||||
persister=None,
|
persister=None,
|
||||||
record_mode="once",
|
record_mode=RecordMode.ONCE,
|
||||||
match_on=(uri, method),
|
match_on=(uri, method),
|
||||||
before_record_request=None,
|
before_record_request=None,
|
||||||
before_record_response=None,
|
before_record_response=None,
|
||||||
custom_patches=(),
|
custom_patches=(),
|
||||||
inject=False,
|
inject=False,
|
||||||
|
allow_playback_repeats=False,
|
||||||
):
|
):
|
||||||
self._persister = persister or FilesystemPersister
|
self._persister = persister or FilesystemPersister
|
||||||
self._path = path
|
self._path = path
|
||||||
@@ -192,6 +194,7 @@ class Cassette:
|
|||||||
self.inject = inject
|
self.inject = inject
|
||||||
self.record_mode = record_mode
|
self.record_mode = record_mode
|
||||||
self.custom_patches = custom_patches
|
self.custom_patches = custom_patches
|
||||||
|
self.allow_playback_repeats = allow_playback_repeats
|
||||||
|
|
||||||
# self.data is the list of (req, resp) tuples
|
# self.data is the list of (req, resp) tuples
|
||||||
self.data = []
|
self.data = []
|
||||||
@@ -206,7 +209,7 @@ class Cassette:
|
|||||||
@property
|
@property
|
||||||
def all_played(self):
|
def all_played(self):
|
||||||
"""Returns True if all responses have been played, False otherwise."""
|
"""Returns True if all responses have been played, False otherwise."""
|
||||||
return self.play_count == len(self)
|
return len(self.play_counts.values()) == len(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def requests(self):
|
def requests(self):
|
||||||
@@ -218,7 +221,7 @@ class Cassette:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def write_protected(self):
|
def write_protected(self):
|
||||||
return self.rewound and self.record_mode == "once" or self.record_mode == "none"
|
return self.rewound and self.record_mode == RecordMode.ONCE or self.record_mode == RecordMode.NONE
|
||||||
|
|
||||||
def append(self, request, response):
|
def append(self, request, response):
|
||||||
"""Add a request, response pair to this cassette"""
|
"""Add a request, response pair to this cassette"""
|
||||||
@@ -250,7 +253,7 @@ class Cassette:
|
|||||||
|
|
||||||
def can_play_response_for(self, request):
|
def can_play_response_for(self, request):
|
||||||
request = self._before_record_request(request)
|
request = self._before_record_request(request)
|
||||||
return request and request in self and self.record_mode != "all" and self.rewound
|
return request and request in self and self.record_mode != RecordMode.ALL and self.rewound
|
||||||
|
|
||||||
def play_response(self, request):
|
def play_response(self, request):
|
||||||
"""
|
"""
|
||||||
@@ -258,7 +261,7 @@ class Cassette:
|
|||||||
hasn't been played back before, and mark it as played
|
hasn't been played back before, and mark it as played
|
||||||
"""
|
"""
|
||||||
for index, response in self._responses(request):
|
for index, response in self._responses(request):
|
||||||
if self.play_counts[index] == 0:
|
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
||||||
self.play_counts[index] += 1
|
self.play_counts[index] += 1
|
||||||
return response
|
return response
|
||||||
# The cassette doesn't contain the request asked for.
|
# The cassette doesn't contain the request asked for.
|
||||||
@@ -348,6 +351,6 @@ class Cassette:
|
|||||||
def __contains__(self, request):
|
def __contains__(self, request):
|
||||||
"""Return whether or not a request has been stored"""
|
"""Return whether or not a request has been stored"""
|
||||||
for index, response in self._responses(request):
|
for index, response in self._responses(request):
|
||||||
if self.play_counts[index] == 0:
|
if self.play_counts[index] == 0 or self.allow_playback_repeats:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .cassette import Cassette
|
|||||||
from .serializers import yamlserializer, jsonserializer
|
from .serializers import yamlserializer, jsonserializer
|
||||||
from .persisters.filesystem import FilesystemPersister
|
from .persisters.filesystem import FilesystemPersister
|
||||||
from .util import compose, auto_decorate
|
from .util import compose, auto_decorate
|
||||||
|
from .record_mode import RecordMode
|
||||||
from . import matchers
|
from . import matchers
|
||||||
from . import filters
|
from . import filters
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ class VCR:
|
|||||||
custom_patches=(),
|
custom_patches=(),
|
||||||
filter_query_parameters=(),
|
filter_query_parameters=(),
|
||||||
ignore_hosts=(),
|
ignore_hosts=(),
|
||||||
record_mode="once",
|
record_mode=RecordMode.ONCE,
|
||||||
ignore_localhost=False,
|
ignore_localhost=False,
|
||||||
filter_headers=(),
|
filter_headers=(),
|
||||||
before_record_response=None,
|
before_record_response=None,
|
||||||
@@ -148,6 +149,7 @@ class VCR:
|
|||||||
"inject": kwargs.get("inject_cassette", self.inject_cassette),
|
"inject": kwargs.get("inject_cassette", self.inject_cassette),
|
||||||
"path_transformer": path_transformer,
|
"path_transformer": path_transformer,
|
||||||
"func_path_generator": func_path_generator,
|
"func_path_generator": func_path_generator,
|
||||||
|
"allow_playback_repeats": kwargs.get("allow_playback_repeats", False),
|
||||||
}
|
}
|
||||||
path = kwargs.get("path")
|
path = kwargs.get("path")
|
||||||
if path:
|
if path:
|
||||||
|
|||||||
25
vcr/patch.py
25
vcr/patch.py
@@ -94,6 +94,15 @@ else:
|
|||||||
_AiohttpClientSessionRequest = aiohttp.client.ClientSession._request
|
_AiohttpClientSessionRequest = aiohttp.client.ClientSession._request
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import httpx
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
_HttpxSyncClient_send = httpx.Client.send
|
||||||
|
_HttpxAsyncClient_send = httpx.AsyncClient.send
|
||||||
|
|
||||||
|
|
||||||
class CassettePatcherBuilder:
|
class CassettePatcherBuilder:
|
||||||
def _build_patchers_from_mock_triples_decorator(function):
|
def _build_patchers_from_mock_triples_decorator(function):
|
||||||
@functools.wraps(function)
|
@functools.wraps(function)
|
||||||
@@ -116,6 +125,7 @@ class CassettePatcherBuilder:
|
|||||||
self._boto(),
|
self._boto(),
|
||||||
self._tornado(),
|
self._tornado(),
|
||||||
self._aiohttp(),
|
self._aiohttp(),
|
||||||
|
self._httpx(),
|
||||||
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
|
self._build_patchers_from_mock_triples(self._cassette.custom_patches),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -313,6 +323,21 @@ class CassettePatcherBuilder:
|
|||||||
new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
|
new_request = vcr_request(self._cassette, _AiohttpClientSessionRequest)
|
||||||
yield client.ClientSession, "_request", new_request
|
yield client.ClientSession, "_request", new_request
|
||||||
|
|
||||||
|
@_build_patchers_from_mock_triples_decorator
|
||||||
|
def _httpx(self):
|
||||||
|
try:
|
||||||
|
import httpx
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
from .stubs.httpx_stubs import async_vcr_send, sync_vcr_send
|
||||||
|
|
||||||
|
new_async_client_send = async_vcr_send(self._cassette, _HttpxAsyncClient_send)
|
||||||
|
yield httpx.AsyncClient, "send", new_async_client_send
|
||||||
|
|
||||||
|
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
|
||||||
|
yield httpx.Client, "send", new_sync_client_send
|
||||||
|
|
||||||
def _urllib3_patchers(self, cpool, stubs):
|
def _urllib3_patchers(self, cpool, stubs):
|
||||||
http_connection_remover = ConnectionRemover(
|
http_connection_remover = ConnectionRemover(
|
||||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
||||||
|
|||||||
23
vcr/record_mode.py
Normal file
23
vcr/record_mode.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class RecordMode(str, Enum):
|
||||||
|
"""
|
||||||
|
Configues when VCR will record to the cassette.
|
||||||
|
|
||||||
|
Can be declared by either using the enumerated value (`vcr.mode.ONCE`)
|
||||||
|
or by simply using the defined string (`once`).
|
||||||
|
|
||||||
|
`ALL`: Every request is recorded.
|
||||||
|
`ANY`: ?
|
||||||
|
`NEW_EPISODES`: Any request not found in the cassette is recorded.
|
||||||
|
`NONE`: No requests are recorded.
|
||||||
|
`ONCE`: First set of requests is recorded, all others are replayed.
|
||||||
|
Attempting to add a new episode fails.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALL = "all"
|
||||||
|
ANY = "any"
|
||||||
|
NEW_EPISODES = "new_episodes"
|
||||||
|
NONE = "none"
|
||||||
|
ONCE = "once"
|
||||||
@@ -5,9 +5,13 @@ import logging
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from aiohttp import ClientConnectionError, ClientResponse, RequestInfo, streams
|
from aiohttp import ClientConnectionError, ClientResponse, RequestInfo, streams
|
||||||
|
from aiohttp import hdrs, CookieJar
|
||||||
|
from http.cookies import CookieError, Morsel, SimpleCookie
|
||||||
|
from aiohttp.helpers import strip_auth_from_url
|
||||||
from multidict import CIMultiDict, CIMultiDictProxy
|
from multidict import CIMultiDict, CIMultiDictProxy
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
from vcr.request import Request
|
from vcr.request import Request
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -59,15 +63,27 @@ def build_response(vcr_request, vcr_response, history):
|
|||||||
request_info = RequestInfo(
|
request_info = RequestInfo(
|
||||||
url=URL(vcr_request.url),
|
url=URL(vcr_request.url),
|
||||||
method=vcr_request.method,
|
method=vcr_request.method,
|
||||||
headers=CIMultiDictProxy(CIMultiDict(vcr_request.headers)),
|
headers=_deserialize_headers(vcr_request.headers),
|
||||||
real_url=URL(vcr_request.url),
|
real_url=URL(vcr_request.url),
|
||||||
)
|
)
|
||||||
response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info)
|
response = MockClientResponse(vcr_request.method, URL(vcr_response.get("url")), request_info=request_info)
|
||||||
response.status = vcr_response["status"]["code"]
|
response.status = vcr_response["status"]["code"]
|
||||||
response._body = vcr_response["body"].get("string", b"")
|
response._body = vcr_response["body"].get("string", b"")
|
||||||
response.reason = vcr_response["status"]["message"]
|
response.reason = vcr_response["status"]["message"]
|
||||||
response._headers = CIMultiDictProxy(CIMultiDict(vcr_response["headers"]))
|
response._headers = _deserialize_headers(vcr_response["headers"])
|
||||||
response._history = tuple(history)
|
response._history = tuple(history)
|
||||||
|
# cookies
|
||||||
|
for hdr in response.headers.getall(hdrs.SET_COOKIE, ()):
|
||||||
|
try:
|
||||||
|
cookies = SimpleCookie(hdr)
|
||||||
|
for cookie_name, cookie in cookies.items():
|
||||||
|
expires = cookie.get("expires", "").strip()
|
||||||
|
if expires:
|
||||||
|
log.debug('Ignoring expiration date: %s="%s"', cookie_name, expires)
|
||||||
|
cookie["expires"] = ""
|
||||||
|
response.cookies.load(cookie.output(header="").strip())
|
||||||
|
except CookieError as exc:
|
||||||
|
log.warning("Can not load response cookies: %s", exc)
|
||||||
|
|
||||||
response.close()
|
response.close()
|
||||||
return response
|
return response
|
||||||
@@ -81,7 +97,23 @@ def _serialize_headers(headers):
|
|||||||
"""
|
"""
|
||||||
# Mark strings as keys so 'istr' types don't show up in
|
# Mark strings as keys so 'istr' types don't show up in
|
||||||
# the cassettes as comments.
|
# the cassettes as comments.
|
||||||
return {str(k): v for k, v in headers.items()}
|
serialized_headers = {}
|
||||||
|
for k, v in headers.items():
|
||||||
|
serialized_headers.setdefault(str(k), []).append(v)
|
||||||
|
|
||||||
|
return serialized_headers
|
||||||
|
|
||||||
|
|
||||||
|
def _deserialize_headers(headers):
|
||||||
|
deserialized_headers = CIMultiDict()
|
||||||
|
for k, vs in headers.items():
|
||||||
|
if isinstance(vs, list):
|
||||||
|
for v in vs:
|
||||||
|
deserialized_headers.add(k, v)
|
||||||
|
else:
|
||||||
|
deserialized_headers.add(k, vs)
|
||||||
|
|
||||||
|
return CIMultiDictProxy(deserialized_headers)
|
||||||
|
|
||||||
|
|
||||||
def play_responses(cassette, vcr_request):
|
def play_responses(cassette, vcr_request):
|
||||||
@@ -92,14 +124,20 @@ def play_responses(cassette, vcr_request):
|
|||||||
# If we're following redirects, continue playing until we reach
|
# If we're following redirects, continue playing until we reach
|
||||||
# our final destination.
|
# our final destination.
|
||||||
while 300 <= response.status <= 399:
|
while 300 <= response.status <= 399:
|
||||||
next_url = URL(response.url).with_path(response.headers["location"])
|
if "location" not in response.headers:
|
||||||
|
break
|
||||||
|
|
||||||
|
next_url = URL(response.url).join(URL(response.headers["location"]))
|
||||||
|
|
||||||
# Make a stub VCR request that we can then use to look up the recorded
|
# Make a stub VCR request that we can then use to look up the recorded
|
||||||
# VCR request saved to the cassette. This feels a little hacky and
|
# VCR request saved to the cassette. This feels a little hacky and
|
||||||
# may have edge cases based on the headers we're providing (e.g. if
|
# may have edge cases based on the headers we're providing (e.g. if
|
||||||
# there's a matcher that is used to filter by headers).
|
# there's a matcher that is used to filter by headers).
|
||||||
vcr_request = Request("GET", str(next_url), None, _serialize_headers(response.request_info.headers))
|
vcr_request = Request("GET", str(next_url), None, _serialize_headers(response.request_info.headers))
|
||||||
vcr_request = cassette.find_requests_with_most_matches(vcr_request)[0][0]
|
vcr_requests = cassette.find_requests_with_most_matches(vcr_request)
|
||||||
|
for vcr_request, *_ in vcr_requests:
|
||||||
|
if cassette.can_play_response_for(vcr_request):
|
||||||
|
break
|
||||||
|
|
||||||
# Tack on the response we saw from the redirect into the history
|
# Tack on the response we saw from the redirect into the history
|
||||||
# list that is added on to the final response.
|
# list that is added on to the final response.
|
||||||
@@ -163,6 +201,33 @@ async def record_responses(cassette, vcr_request, response):
|
|||||||
await record_response(cassette, vcr_request, response)
|
await record_response(cassette, vcr_request, response)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_cookie_header(session, cookies, cookie_header, url):
|
||||||
|
url, _ = strip_auth_from_url(url)
|
||||||
|
all_cookies = session._cookie_jar.filter_cookies(url)
|
||||||
|
if cookies is not None:
|
||||||
|
tmp_cookie_jar = CookieJar()
|
||||||
|
tmp_cookie_jar.update_cookies(cookies)
|
||||||
|
req_cookies = tmp_cookie_jar.filter_cookies(url)
|
||||||
|
if req_cookies:
|
||||||
|
all_cookies.load(req_cookies)
|
||||||
|
|
||||||
|
if not all_cookies and not cookie_header:
|
||||||
|
return None
|
||||||
|
|
||||||
|
c = SimpleCookie()
|
||||||
|
if cookie_header:
|
||||||
|
c.load(cookie_header)
|
||||||
|
for name, value in all_cookies.items():
|
||||||
|
if isinstance(value, Morsel):
|
||||||
|
mrsl_val = value.get(value.key, Morsel())
|
||||||
|
mrsl_val.set(value.key, value.value, value.coded_value)
|
||||||
|
c[name] = mrsl_val
|
||||||
|
else:
|
||||||
|
c[name] = value
|
||||||
|
|
||||||
|
return c.output(header="", sep=";").strip()
|
||||||
|
|
||||||
|
|
||||||
def vcr_request(cassette, real_request):
|
def vcr_request(cassette, real_request):
|
||||||
@functools.wraps(real_request)
|
@functools.wraps(real_request)
|
||||||
async def new_request(self, method, url, **kwargs):
|
async def new_request(self, method, url, **kwargs):
|
||||||
@@ -171,6 +236,7 @@ def vcr_request(cassette, real_request):
|
|||||||
headers = self._prepare_headers(headers)
|
headers = self._prepare_headers(headers)
|
||||||
data = kwargs.get("data", kwargs.get("json"))
|
data = kwargs.get("data", kwargs.get("json"))
|
||||||
params = kwargs.get("params")
|
params = kwargs.get("params")
|
||||||
|
cookies = kwargs.get("cookies")
|
||||||
|
|
||||||
if auth is not None:
|
if auth is not None:
|
||||||
headers["AUTHORIZATION"] = auth.encode()
|
headers["AUTHORIZATION"] = auth.encode()
|
||||||
@@ -181,22 +247,23 @@ def vcr_request(cassette, real_request):
|
|||||||
params[k] = str(v)
|
params[k] = str(v)
|
||||||
request_url = URL(url).with_query(params)
|
request_url = URL(url).with_query(params)
|
||||||
|
|
||||||
vcr_request = Request(method, str(request_url), data, headers)
|
c_header = headers.pop(hdrs.COOKIE, None)
|
||||||
|
cookie_header = _build_cookie_header(self, cookies, c_header, request_url)
|
||||||
|
if cookie_header:
|
||||||
|
headers[hdrs.COOKIE] = cookie_header
|
||||||
|
|
||||||
|
vcr_request = Request(method, str(request_url), data, _serialize_headers(headers))
|
||||||
|
|
||||||
if cassette.can_play_response_for(vcr_request):
|
if cassette.can_play_response_for(vcr_request):
|
||||||
return play_responses(cassette, vcr_request)
|
log.info("Playing response for {} from cassette".format(vcr_request))
|
||||||
|
response = play_responses(cassette, vcr_request)
|
||||||
|
for redirect in response.history:
|
||||||
|
self._cookie_jar.update_cookies(redirect.cookies, redirect.url)
|
||||||
|
self._cookie_jar.update_cookies(response.cookies, response.url)
|
||||||
|
return response
|
||||||
|
|
||||||
if cassette.write_protected and cassette.filter_request(vcr_request):
|
if cassette.write_protected and cassette.filter_request(vcr_request):
|
||||||
response = MockClientResponse(method, URL(url))
|
raise CannotOverwriteExistingCassetteException(cassette=cassette, failed_request=vcr_request)
|
||||||
response.status = 599
|
|
||||||
msg = (
|
|
||||||
"No match for the request {!r} was found. Can't overwrite "
|
|
||||||
"existing cassette {!r} in your current record mode {!r}."
|
|
||||||
)
|
|
||||||
msg = msg.format(vcr_request, cassette._path, cassette.record_mode)
|
|
||||||
response._body = msg.encode()
|
|
||||||
response.close()
|
|
||||||
return response
|
|
||||||
|
|
||||||
log.info("%s not in cassette, sending to real server", vcr_request)
|
log.info("%s not in cassette, sending to real server", vcr_request)
|
||||||
|
|
||||||
159
vcr/stubs/httpx_stubs.py
Normal file
159
vcr/stubs/httpx_stubs.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from vcr.request import Request as VcrRequest
|
||||||
|
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||||
|
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_headers(httpx_reponse):
|
||||||
|
"""
|
||||||
|
Some headers can appear multiple times, like "Set-Cookie".
|
||||||
|
Therefore transform to every header key to list of values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
out = {}
|
||||||
|
for key, var in httpx_reponse.headers.raw:
|
||||||
|
decoded_key = key.decode("utf-8")
|
||||||
|
out.setdefault(decoded_key, [])
|
||||||
|
out[decoded_key].append(var.decode("utf-8"))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _to_serialized_response(httpx_reponse):
|
||||||
|
return {
|
||||||
|
"status_code": httpx_reponse.status_code,
|
||||||
|
"http_version": httpx_reponse.http_version,
|
||||||
|
"headers": _transform_headers(httpx_reponse),
|
||||||
|
"content": httpx_reponse.content.decode("utf-8", "ignore"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _from_serialized_headers(headers):
|
||||||
|
"""
|
||||||
|
httpx accepts headers as list of tuples of header key and value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
header_list = []
|
||||||
|
for key, values in headers.items():
|
||||||
|
for v in values:
|
||||||
|
header_list.append((key, v))
|
||||||
|
return header_list
|
||||||
|
|
||||||
|
|
||||||
|
@patch("httpx.Response.close", MagicMock())
|
||||||
|
@patch("httpx.Response.read", MagicMock())
|
||||||
|
def _from_serialized_response(request, serialized_response, history=None):
|
||||||
|
content = serialized_response.get("content").encode()
|
||||||
|
response = httpx.Response(
|
||||||
|
status_code=serialized_response.get("status_code"),
|
||||||
|
request=request,
|
||||||
|
http_version=serialized_response.get("http_version"),
|
||||||
|
headers=_from_serialized_headers(serialized_response.get("headers")),
|
||||||
|
content=content,
|
||||||
|
history=history or [],
|
||||||
|
)
|
||||||
|
response._content = content
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def _make_vcr_request(httpx_request, **kwargs):
|
||||||
|
body = httpx_request.read().decode("utf-8")
|
||||||
|
uri = str(httpx_request.url)
|
||||||
|
headers = dict(httpx_request.headers)
|
||||||
|
return VcrRequest(httpx_request.method, uri, body, headers)
|
||||||
|
|
||||||
|
|
||||||
|
def _shared_vcr_send(cassette, real_send, *args, **kwargs):
|
||||||
|
real_request = args[1]
|
||||||
|
|
||||||
|
vcr_request = _make_vcr_request(real_request, **kwargs)
|
||||||
|
|
||||||
|
if cassette.can_play_response_for(vcr_request):
|
||||||
|
return vcr_request, _play_responses(cassette, real_request, vcr_request, args[0], kwargs)
|
||||||
|
|
||||||
|
if cassette.write_protected and cassette.filter_request(vcr_request):
|
||||||
|
raise CannotOverwriteExistingCassetteException(cassette=cassette, failed_request=vcr_request)
|
||||||
|
|
||||||
|
_logger.info("%s not in cassette, sending to real server", vcr_request)
|
||||||
|
return vcr_request, None
|
||||||
|
|
||||||
|
|
||||||
|
def _record_responses(cassette, vcr_request, real_response):
|
||||||
|
for past_real_response in real_response.history:
|
||||||
|
past_vcr_request = _make_vcr_request(past_real_response.request)
|
||||||
|
cassette.append(past_vcr_request, _to_serialized_response(past_real_response))
|
||||||
|
|
||||||
|
if real_response.history:
|
||||||
|
# If there was a redirection keep we want the request which will hold the
|
||||||
|
# final redirect value
|
||||||
|
vcr_request = _make_vcr_request(real_response.request)
|
||||||
|
|
||||||
|
cassette.append(vcr_request, _to_serialized_response(real_response))
|
||||||
|
return real_response
|
||||||
|
|
||||||
|
|
||||||
|
def _play_responses(cassette, request, vcr_request, client, kwargs):
|
||||||
|
history = []
|
||||||
|
allow_redirects = kwargs.get("allow_redirects", True)
|
||||||
|
vcr_response = cassette.play_response(vcr_request)
|
||||||
|
response = _from_serialized_response(request, vcr_response)
|
||||||
|
|
||||||
|
while allow_redirects and 300 <= response.status_code <= 399:
|
||||||
|
next_url = response.headers.get("location")
|
||||||
|
if not next_url:
|
||||||
|
break
|
||||||
|
|
||||||
|
vcr_request = VcrRequest("GET", next_url, None, dict(response.headers))
|
||||||
|
vcr_request = cassette.find_requests_with_most_matches(vcr_request)[0][0]
|
||||||
|
|
||||||
|
history.append(response)
|
||||||
|
# add cookies from response to session cookie store
|
||||||
|
client.cookies.extract_cookies(response)
|
||||||
|
|
||||||
|
vcr_response = cassette.play_response(vcr_request)
|
||||||
|
response = _from_serialized_response(vcr_request, vcr_response, history)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_vcr_send(cassette, real_send, *args, **kwargs):
|
||||||
|
vcr_request, response = _shared_vcr_send(cassette, real_send, *args, **kwargs)
|
||||||
|
if response:
|
||||||
|
# add cookies from response to session cookie store
|
||||||
|
args[0].cookies.extract_cookies(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
real_response = await real_send(*args, **kwargs)
|
||||||
|
return _record_responses(cassette, vcr_request, real_response)
|
||||||
|
|
||||||
|
|
||||||
|
def async_vcr_send(cassette, real_send):
|
||||||
|
@functools.wraps(real_send)
|
||||||
|
def _inner_send(*args, **kwargs):
|
||||||
|
return _async_vcr_send(cassette, real_send, *args, **kwargs)
|
||||||
|
|
||||||
|
return _inner_send
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_vcr_send(cassette, real_send, *args, **kwargs):
|
||||||
|
vcr_request, response = _shared_vcr_send(cassette, real_send, *args, **kwargs)
|
||||||
|
if response:
|
||||||
|
# add cookies from response to session cookie store
|
||||||
|
args[0].cookies.extract_cookies(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
real_response = real_send(*args, **kwargs)
|
||||||
|
return _record_responses(cassette, vcr_request, real_response)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_vcr_send(cassette, real_send):
|
||||||
|
@functools.wraps(real_send)
|
||||||
|
def _inner_send(*args, **kwargs):
|
||||||
|
return _sync_vcr_send(cassette, real_send, *args, **kwargs)
|
||||||
|
|
||||||
|
return _inner_send
|
||||||
Reference in New Issue
Block a user