Skip to content

Commit 858b925

Browse files
authored
Add 'dry_mode' parameter (#382)
1 parent 07dfd16 commit 858b925

File tree

7 files changed

+90
-23
lines changed

7 files changed

+90
-23
lines changed

README.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ An asynchronous client is also available. It has all the same endpoints as the s
8080
info = await client.server_info()
8181
assert 'schema' in info['capabilities'], "Server doesn't support schema validation."
8282
83+
84+
Dry Mode
85+
--------
86+
87+
The ``dry_mode`` parameter can be set to simulate requests without actually sending them over the network.
88+
When enabled, dry mode ensures that no external calls are made, making it useful for testing or debugging.
89+
Instead of performing real HTTP operations, the client logs the requests with ``DEBUG`` level.
90+
91+
8392
Using a Bearer access token to authenticate (OpenID)
8493
----------------------------------------------------
8594

src/kinto_http/batch.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ def send(self):
7979
method="POST", endpoint=self.endpoints.get("batch"), payload={"requests": chunk}
8080
)
8181
resp, headers = self.session.request(**kwargs)
82+
if self.session.dry_mode:
83+
resp.setdefault(
84+
"responses", [{"status": 200, "body": {}} for i in range(len(chunk))]
85+
)
8286
for i, response in enumerate(resp["responses"]):
8387
status_code = response["status"]
8488

src/kinto_http/client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __init__(
4646
timeout=None,
4747
ignore_batch_4xx=False,
4848
headers=None,
49+
dry_mode=False,
4950
):
5051
self.endpoints = Endpoints()
5152

@@ -63,6 +64,7 @@ def __init__(
6364
retry_after=retry_after,
6465
timeout=timeout,
6566
headers=headers,
67+
dry_mode=dry_mode,
6668
)
6769
self.session = create_session(**session_kwargs)
6870
self.bucket_name = bucket
@@ -88,9 +90,11 @@ def clone(self, **kwargs):
8890
def batch(self, **kwargs):
8991
if self._server_settings is None:
9092
resp, _ = self.session.request("GET", self._get_endpoint("root"))
91-
self._server_settings = resp["settings"]
93+
self._server_settings = resp["settings"] if not self.session.dry_mode else {}
9294

93-
batch_max_requests = self._server_settings["batch_max_requests"]
95+
batch_max_requests = (
96+
self._server_settings["batch_max_requests"] if not self.session.dry_mode else 999999
97+
)
9498
batch_session = BatchSession(
9599
self, batch_max_requests=batch_max_requests, ignore_4xx_errors=self._ignore_batch_4xx
96100
)

src/kinto_http/session.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import json
2+
import logging
23
import time
34
import warnings
4-
from urllib.parse import urlparse
5+
from urllib.parse import urlencode, urlparse
56

67
import requests
8+
from urllib3.response import HTTPResponse
79

810
import kinto_http
911
from kinto_http import utils
1012
from kinto_http.constants import USER_AGENT
1113
from kinto_http.exceptions import BackoffException, KintoException
1214

1315

16+
logger = logging.getLogger(__name__)
17+
18+
1419
def create_session(server_url=None, auth=None, session=None, **kwargs):
1520
"""Returns a session from the passed arguments.
1621
@@ -55,7 +60,14 @@ class Session(object):
5560
"""Handles all the interactions with the network."""
5661

5762
def __init__(
58-
self, server_url, auth=None, timeout=False, headers=None, retry=0, retry_after=None
63+
self,
64+
server_url,
65+
auth=None,
66+
timeout=False,
67+
headers=None,
68+
retry=0,
69+
retry_after=None,
70+
dry_mode=False,
5971
):
6072
self.backoff = None
6173
self.server_url = server_url
@@ -64,6 +76,7 @@ def __init__(
6476
self.retry_after = retry_after
6577
self.timeout = timeout
6678
self.headers = headers or {}
79+
self.dry_mode = dry_mode
6780

6881
def request(self, method, endpoint, data=None, permissions=None, payload=None, **kwargs):
6982
current_time = time.time()
@@ -123,7 +136,15 @@ def request(self, method, endpoint, data=None, permissions=None, payload=None, *
123136

124137
retry = self.nb_retry
125138
while retry >= 0:
126-
resp = requests.request(method, actual_url, **kwargs)
139+
if self.dry_mode:
140+
qs = ("?" + urlencode(kwargs["params"])) if "params" in kwargs else ""
141+
logger.debug(f"(dry mode) {method} {actual_url}{qs}")
142+
resp = HTTPResponse(
143+
status=200, headers={"Content-Type": "application/json"}, body=b"{}"
144+
)
145+
resp.status_code = resp.status
146+
else:
147+
resp = requests.request(method, actual_url, **kwargs)
127148

128149
if "Alert" in resp.headers:
129150
warnings.warn(resp.headers["Alert"], DeprecationWarning)

tests/conftest.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,37 @@
1616

1717

1818
@pytest.fixture
19-
def async_client_setup(mocker: MockerFixture) -> AsyncClient:
19+
def mocked_session(mocker: MockerFixture):
2020
session = mocker.MagicMock()
21-
mock_response(session)
22-
client = AsyncClient(session=session, bucket="mybucket")
21+
session.dry_mode = False
22+
return session
23+
24+
25+
@pytest.fixture
26+
def async_client_setup(mocked_session, mocker: MockerFixture) -> AsyncClient:
27+
mock_response(mocked_session)
28+
client = AsyncClient(session=mocked_session, bucket="mybucket")
2329
return client
2430

2531

2632
@pytest.fixture
27-
def client_setup(mocker: MockerFixture) -> Client:
28-
session = mocker.MagicMock()
29-
mock_response(session)
30-
client = Client(session=session, bucket="mybucket")
33+
def client_setup(mocked_session, mocker: MockerFixture) -> Client:
34+
mock_response(mocked_session)
35+
client = Client(session=mocked_session, bucket="mybucket")
3136
return client
3237

3338

3439
@pytest.fixture
35-
def record_async_setup(mocker: MockerFixture) -> AsyncClient:
36-
session = mocker.MagicMock()
37-
session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count)
38-
client = AsyncClient(session=session, bucket="mybucket", collection="mycollection")
40+
def record_async_setup(mocked_session, mocker: MockerFixture) -> AsyncClient:
41+
mocked_session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count)
42+
client = AsyncClient(session=mocked_session, bucket="mybucket", collection="mycollection")
3943
return client
4044

4145

4246
@pytest.fixture
43-
def record_setup(mocker: MockerFixture) -> Client:
44-
session = mocker.MagicMock()
45-
session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count)
46-
client = Client(session=session, bucket="mybucket", collection="mycollection")
47+
def record_setup(mocked_session, mocker: MockerFixture) -> Client:
48+
mocked_session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count)
49+
client = Client(session=mocked_session, bucket="mybucket", collection="mycollection")
4750
return client
4851

4952

@@ -87,10 +90,11 @@ def endpoints_setup() -> Tuple[Endpoints, Dict]:
8790

8891

8992
@pytest.fixture
90-
def batch_setup(mocker: MockerFixture) -> Client:
91-
client = mocker.MagicMock()
93+
def batch_setup(mocked_session, mocker: MockerFixture) -> Client:
9294
mocker.sentinel.resp = {"responses": []}
93-
client.session.request.return_value = (mocker.sentinel.resp, mocker.sentinel.headers)
95+
mocked_session.request.return_value = (mocker.sentinel.resp, mocker.sentinel.headers)
96+
client = mocker.MagicMock()
97+
client.session = mocked_session
9498
return client
9599

96100

tests/test_functional.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,3 +590,16 @@ def test_get_permissions(functional_setup):
590590

591591
perms_by_uri = {p["uri"]: p for p in perms}
592592
assert set(perms_by_uri["/accounts/user"]["permissions"]) == {"read", "write"}
593+
594+
595+
def test_dry_mode(functional_setup):
596+
client = functional_setup.clone(server_url="http://not-a-valid-domain:42", dry_mode=True)
597+
598+
with client.batch() as batch:
599+
batch.create_bucket()
600+
batch.create_collection(id="cid")
601+
602+
r1, r2 = batch.results()
603+
604+
assert r1 == {}
605+
assert r2 == {}

tests/test_session.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import sys
23
import time
34
import warnings
@@ -545,3 +546,14 @@ def test_next_request_without_the_header_clear_the_backoff(
545546
time.sleep(1) # Spend the backoff
546547
session.request("get", "/test") # The second call reset the backoff
547548
assert session.backoff is None
549+
550+
551+
def test_dry_mode_logs_debug(caplog):
552+
caplog.set_level(logging.DEBUG)
553+
554+
session = Session(server_url="https://foo:42", dry_mode=True)
555+
body, headers = session.request("GET", "/test", params={"_since": "333"})
556+
557+
assert body == {}
558+
assert headers == {"Content-Type": "application/json"}
559+
assert caplog.messages == ["(dry mode) GET https://foo:42/test?_since=333"]

0 commit comments

Comments
 (0)