Skip to content

Commit

Permalink
Add 'dry_mode' parameter (#382)
Browse files Browse the repository at this point in the history
  • Loading branch information
leplatrem authored Dec 3, 2024
1 parent 07dfd16 commit 858b925
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 23 deletions.
9 changes: 9 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ An asynchronous client is also available. It has all the same endpoints as the s
info = await client.server_info()
assert 'schema' in info['capabilities'], "Server doesn't support schema validation."
Dry Mode
--------

The ``dry_mode`` parameter can be set to simulate requests without actually sending them over the network.
When enabled, dry mode ensures that no external calls are made, making it useful for testing or debugging.
Instead of performing real HTTP operations, the client logs the requests with ``DEBUG`` level.


Using a Bearer access token to authenticate (OpenID)
----------------------------------------------------

Expand Down
4 changes: 4 additions & 0 deletions src/kinto_http/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ def send(self):
method="POST", endpoint=self.endpoints.get("batch"), payload={"requests": chunk}
)
resp, headers = self.session.request(**kwargs)
if self.session.dry_mode:
resp.setdefault(
"responses", [{"status": 200, "body": {}} for i in range(len(chunk))]
)
for i, response in enumerate(resp["responses"]):
status_code = response["status"]

Expand Down
8 changes: 6 additions & 2 deletions src/kinto_http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(
timeout=None,
ignore_batch_4xx=False,
headers=None,
dry_mode=False,
):
self.endpoints = Endpoints()

Expand All @@ -63,6 +64,7 @@ def __init__(
retry_after=retry_after,
timeout=timeout,
headers=headers,
dry_mode=dry_mode,
)
self.session = create_session(**session_kwargs)
self.bucket_name = bucket
Expand All @@ -88,9 +90,11 @@ def clone(self, **kwargs):
def batch(self, **kwargs):
if self._server_settings is None:
resp, _ = self.session.request("GET", self._get_endpoint("root"))
self._server_settings = resp["settings"]
self._server_settings = resp["settings"] if not self.session.dry_mode else {}

batch_max_requests = self._server_settings["batch_max_requests"]
batch_max_requests = (
self._server_settings["batch_max_requests"] if not self.session.dry_mode else 999999
)
batch_session = BatchSession(
self, batch_max_requests=batch_max_requests, ignore_4xx_errors=self._ignore_batch_4xx
)
Expand Down
27 changes: 24 additions & 3 deletions src/kinto_http/session.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import json
import logging
import time
import warnings
from urllib.parse import urlparse
from urllib.parse import urlencode, urlparse

import requests
from urllib3.response import HTTPResponse

import kinto_http
from kinto_http import utils
from kinto_http.constants import USER_AGENT
from kinto_http.exceptions import BackoffException, KintoException


logger = logging.getLogger(__name__)


def create_session(server_url=None, auth=None, session=None, **kwargs):
"""Returns a session from the passed arguments.
Expand Down Expand Up @@ -55,7 +60,14 @@ class Session(object):
"""Handles all the interactions with the network."""

def __init__(
self, server_url, auth=None, timeout=False, headers=None, retry=0, retry_after=None
self,
server_url,
auth=None,
timeout=False,
headers=None,
retry=0,
retry_after=None,
dry_mode=False,
):
self.backoff = None
self.server_url = server_url
Expand All @@ -64,6 +76,7 @@ def __init__(
self.retry_after = retry_after
self.timeout = timeout
self.headers = headers or {}
self.dry_mode = dry_mode

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

retry = self.nb_retry
while retry >= 0:
resp = requests.request(method, actual_url, **kwargs)
if self.dry_mode:
qs = ("?" + urlencode(kwargs["params"])) if "params" in kwargs else ""
logger.debug(f"(dry mode) {method} {actual_url}{qs}")
resp = HTTPResponse(
status=200, headers={"Content-Type": "application/json"}, body=b"{}"
)
resp.status_code = resp.status
else:
resp = requests.request(method, actual_url, **kwargs)

if "Alert" in resp.headers:
warnings.warn(resp.headers["Alert"], DeprecationWarning)
Expand Down
40 changes: 22 additions & 18 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,37 @@


@pytest.fixture
def async_client_setup(mocker: MockerFixture) -> AsyncClient:
def mocked_session(mocker: MockerFixture):
session = mocker.MagicMock()
mock_response(session)
client = AsyncClient(session=session, bucket="mybucket")
session.dry_mode = False
return session


@pytest.fixture
def async_client_setup(mocked_session, mocker: MockerFixture) -> AsyncClient:
mock_response(mocked_session)
client = AsyncClient(session=mocked_session, bucket="mybucket")
return client


@pytest.fixture
def client_setup(mocker: MockerFixture) -> Client:
session = mocker.MagicMock()
mock_response(session)
client = Client(session=session, bucket="mybucket")
def client_setup(mocked_session, mocker: MockerFixture) -> Client:
mock_response(mocked_session)
client = Client(session=mocked_session, bucket="mybucket")
return client


@pytest.fixture
def record_async_setup(mocker: MockerFixture) -> AsyncClient:
session = mocker.MagicMock()
session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count)
client = AsyncClient(session=session, bucket="mybucket", collection="mycollection")
def record_async_setup(mocked_session, mocker: MockerFixture) -> AsyncClient:
mocked_session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count)
client = AsyncClient(session=mocked_session, bucket="mybucket", collection="mycollection")
return client


@pytest.fixture
def record_setup(mocker: MockerFixture) -> Client:
session = mocker.MagicMock()
session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count)
client = Client(session=session, bucket="mybucket", collection="mycollection")
def record_setup(mocked_session, mocker: MockerFixture) -> Client:
mocked_session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count)
client = Client(session=mocked_session, bucket="mybucket", collection="mycollection")
return client


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


@pytest.fixture
def batch_setup(mocker: MockerFixture) -> Client:
client = mocker.MagicMock()
def batch_setup(mocked_session, mocker: MockerFixture) -> Client:
mocker.sentinel.resp = {"responses": []}
client.session.request.return_value = (mocker.sentinel.resp, mocker.sentinel.headers)
mocked_session.request.return_value = (mocker.sentinel.resp, mocker.sentinel.headers)
client = mocker.MagicMock()
client.session = mocked_session
return client


Expand Down
13 changes: 13 additions & 0 deletions tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,3 +590,16 @@ def test_get_permissions(functional_setup):

perms_by_uri = {p["uri"]: p for p in perms}
assert set(perms_by_uri["/accounts/user"]["permissions"]) == {"read", "write"}


def test_dry_mode(functional_setup):
client = functional_setup.clone(server_url="http://not-a-valid-domain:42", dry_mode=True)

with client.batch() as batch:
batch.create_bucket()
batch.create_collection(id="cid")

r1, r2 = batch.results()

assert r1 == {}
assert r2 == {}
12 changes: 12 additions & 0 deletions tests/test_session.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import sys
import time
import warnings
Expand Down Expand Up @@ -545,3 +546,14 @@ def test_next_request_without_the_header_clear_the_backoff(
time.sleep(1) # Spend the backoff
session.request("get", "/test") # The second call reset the backoff
assert session.backoff is None


def test_dry_mode_logs_debug(caplog):
caplog.set_level(logging.DEBUG)

session = Session(server_url="https://foo:42", dry_mode=True)
body, headers = session.request("GET", "/test", params={"_since": "333"})

assert body == {}
assert headers == {"Content-Type": "application/json"}
assert caplog.messages == ["(dry mode) GET https://foo:42/test?_since=333"]

0 comments on commit 858b925

Please sign in to comment.