Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ keywords = [
"scraping",
]
dependencies = [
"apify-client>=3.0.0,<4.0.0",
# TODO: Switch to `apify-client[brotli]>=3.1.0,<4.0.0` once apify-client v3.1 is released.
# Until then, use the latest beta that already ships the `brotli` extra so brotli request body
# compression is enabled by default (falling back to gzip when brotli isn't installed).
"apify-client[brotli]>=3.0.7b1,<4.0.0",
"crawlee>=1.8.0,<2.0.0",
"cachetools>=5.5.0",
"cryptography>=42.0.0",
Expand Down
1 change: 1 addition & 0 deletions src/apify/_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ def new_client(
client_kwargs: dict[str, Any] = {
'token': token or self.configuration.token,
'api_url': api_url or self.configuration.api_base_url,
'compression': 'brotli',
}
if max_retries is not None:
client_kwargs['max_retries'] = max_retries
Expand Down
1 change: 1 addition & 0 deletions src/apify/storage_clients/_apify/_api_client_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,5 @@ def _create_api_client(configuration: Configuration) -> ApifyClientAsync:
api_url=configuration.api_base_url,
api_public_url=configuration.api_public_base_url,
max_retries=8,
compression='brotli',
)
58 changes: 58 additions & 0 deletions tests/unit/storage_clients/test_api_client_creation.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
from __future__ import annotations

import json
from typing import TYPE_CHECKING, Any

import brotli
import pytest

from apify import Actor
from apify._configuration import Configuration
from apify.storage_clients._apify._api_client_creation import _create_api_client, create_storage_api_client

if TYPE_CHECKING:
from collections.abc import Callable

from pytest_httpserver import HTTPServer
from werkzeug import Request, Response

from apify_client import ApifyClientAsync


def test_create_api_client_without_token() -> None:
"""Test that _create_api_client raises ValueError when no token is set."""
Expand Down Expand Up @@ -50,3 +63,48 @@ async def test_create_storage_unknown_type() -> None:
storage_type='UnknownType',
configuration=config,
)


def _client_from_configuration(api_url: str) -> ApifyClientAsync:
"""Build an API client the way storage clients do, via `_create_api_client`."""
config = Configuration(token='test-token', api_base_url=api_url, api_public_base_url=api_url)
return _create_api_client(config)


def _client_from_new_client(api_url: str) -> ApifyClientAsync:
"""Build an API client the way user code does, via `Actor.new_client`."""
return Actor.new_client(token='test-token', api_url=api_url)


@pytest.mark.parametrize(
'client_factory',
[
pytest.param(_client_from_configuration, id='storage_api_client'),
pytest.param(_client_from_new_client, id='actor_new_client'),
],
)
async def test_sdk_client_compresses_request_body_with_brotli(
httpserver: HTTPServer,
client_factory: Callable[[str], ApifyClientAsync],
) -> None:
"""API clients created by the SDK compress request bodies with brotli (Content-Encoding: br)."""
api_url = str(httpserver.url_for('/')).removesuffix('/')
client = client_factory(api_url)

captured: dict[str, Any] = {}

def request_handler(request: Request, response: Response) -> Response:
captured['content_encoding'] = request.headers.get('Content-Encoding')
captured['body'] = request.get_data()
return response

httpserver.expect_request('/v2/datasets/test-dataset/items', method='POST').with_post_hook(
request_handler
).respond_with_json({'data': {}}, status=201)

items = [{'hello': 'world'}, {'answer': 42}]
await client.dataset(dataset_id='test-dataset').push_items(items)

assert captured['content_encoding'] == 'br'
# The body must be genuinely brotli-compressed and round-trip back to the original items.
assert json.loads(brotli.decompress(captured['body'])) == items
63 changes: 58 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading