Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f26780a
feat: Compress requests using brotli algo
mixalturek Jul 8, 2026
8e112bb
feat: Apply review comments
mixalturek Jul 8, 2026
0dd99a9
feat: Refactoring of tests
mixalturek Jul 8, 2026
3fffa62
feat: Use brotli quality=6; lower but faster
mixalturek Jul 8, 2026
37f5a47
rm brotli from dev deps
vdusek Jul 9, 2026
7617ed0
feat: Documentation for the compression
mixalturek Jul 9, 2026
28f34e2
feat: Formatting
mixalturek Jul 9, 2026
10c528c
feat: Apply review comments
mixalturek Jul 10, 2026
284711c
feat: Make compression algo and quality configurable
mixalturek Jul 10, 2026
cdcbac3
feat: Fix formatting
mixalturek Jul 10, 2026
3f6373d
feat: Refactor the implementation to use hierarchy of classes
mixalturek Jul 14, 2026
2bf86e3
feat: Fix elif/else, doc update, guard checks
mixalturek Jul 14, 2026
d645984
feat: Revert changes in tests
mixalturek Jul 14, 2026
89ce717
feat: Make the tests parameterized
mixalturek Jul 14, 2026
25d7afa
feat: Polishing
mixalturek Jul 14, 2026
a0650b6
refactor: Split _utils module into a _utils subpackage
vdusek Jul 14, 2026
f7f6a34
refactor: Delegate compressor quality validation to the underlying mo…
vdusek Jul 14, 2026
9fc37df
docs: Polish the compression concept guide
vdusek Jul 14, 2026
0cf66e3
test: Add compressor tests and parametrize charge tests by encoding
vdusek Jul 14, 2026
0a76634
refactor: Unify compression naming and extract compressor resolution
vdusek Jul 14, 2026
29f5315
test: Align compressor tests with renamed param and terminology
vdusek Jul 14, 2026
1feee71
docs: Rename compression page to HTTP compression and trim install docs
vdusek Jul 14, 2026
4674110
build: Format brotli optional-dependency on a single line
vdusek Jul 14, 2026
ff4f21e
feat: Validate compressor quality range and fail fast
vdusek Jul 14, 2026
a90bb02
test: Use pytest.param with explicit ids in compression tests
vdusek Jul 14, 2026
72c4e74
docs: Apply review suggestions to HTTP compression page
vdusek Jul 15, 2026
dfbaca0
refactor: Validate compression eagerly at client construction
vdusek Jul 15, 2026
6596cef
refactor: Align try_import with crawlee-python
vdusek Jul 15, 2026
273c09f
style: Keep future annotations import in try_import
vdusek Jul 15, 2026
c810cfa
test: Add missing future annotations import to unit tests
vdusek Jul 15, 2026
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@

or any other Python package manager that consumes PyPI.

The client compresses request bodies with `gzip` by default (no extra dependencies required). To opt in to
`brotli` (better compression ratio), install the optional extra and pass `compression='brotli'`:

```bash
pip install "apify-client[brotli]"
# or
uv add "apify-client[brotli]"
```

- From [conda-forge](https://anaconda.org/conda-forge/apify-client), it can be installed with [conda](https://docs.conda.io/en/latest/):

Expand Down
17 changes: 17 additions & 0 deletions docs/01_introduction/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ The Apify client is available as the `apify-client` package [on PyPI](https://py
</TabItem>
</Tabs>

For better request-body compression, opt in to `brotli`, which compresses better than `gzip`, especially for large payloads. Install the optional extra:

<Tabs>
<TabItem value="PyPI" label="PyPI" default>
```bash
pip install "apify-client[brotli]"
```
</TabItem>
<TabItem value="conda-forge" label="conda-forge">
```bash
conda install conda-forge::apify-client conda-forge::brotli
```
</TabItem>
</Tabs>

For details, see [HTTP compression](../02_concepts/13_http_compression.mdx).

## Quick example

The following example shows how to run an Actor and retrieve its results:
Expand Down
95 changes: 95 additions & 0 deletions docs/02_concepts/13_http_compression.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
id: http-compression
title: HTTP compression
description: The client compresses every request body automatically using gzip by default, with optional brotli via an explicit opt-in.
---

The Apify client compresses every request body before sending it to the API. It reduces the amount of data transferred over the network, resulting in faster requests and lower bandwidth usage, especially for large payloads such as Actor inputs, dataset uploads, or key-value store records.

## How it works

The client compresses request bodies using the compressor configured via the `compression` parameter (default `'gzip'`). The server supports both gzip and brotli and decompresses the request body transparently.

## Configuration

To choose the compression algorithm, pass `compression` to the client constructor:

```python
from apify_client import ApifyClient

# Default: gzip, no extra dependency required
client = ApifyClient(token='MY-APIFY-TOKEN')

# Opt in to brotli (requires apify-client[brotli])
client = ApifyClient(token='MY-APIFY-TOKEN', compression='brotli')
```

## Enabling brotli

Brotli is available as an optional extra. Install it alongside the client:

```bash
pip install "apify-client[brotli]"
# or
uv add "apify-client[brotli]"
```

Then pass `compression='brotli'` to the client constructor. If you request brotli without installing the extra, the client raises a clear `ImportError`. There is no silent fallback.

## Custom quality and advanced control

For fine-grained control over compression quality, inject an `HttpCompressor` instance directly instead of a string literal:

```python
from apify_client import ApifyClient
from apify_client.http_compressors import BrotliHttpCompressor, GzipHttpCompressor

# Brotli at maximum quality
client = ApifyClient(token='MY-APIFY-TOKEN', compression=BrotliHttpCompressor(quality=11))

# Gzip at maximum quality
client = ApifyClient(token='MY-APIFY-TOKEN', compression=GzipHttpCompressor(quality=9))
```

You can also implement a fully custom compressor by subclassing `HttpCompressor`:

```python
from apify_client import ApifyClient
from apify_client.http_compressors import HttpCompressor


class IdentityCompressor(HttpCompressor):
content_encoding = 'identity'
"""Value sent in the `Content-Encoding` header."""

def compress(self, data: bytes) -> bytes:
"""Compress a request body.

Args:
data: The raw bytes to compress.

Returns:
The compressed bytes.
"""
return data


client = ApifyClient(token='MY-APIFY-TOKEN', compression=IdentityCompressor())
```

## Comparison

| | Brotli | Gzip |
|-------------------------------|-----------------------------------------|---------------------------------|
| **Compression ratio** | Typically better than gzip | Good |
| **CPU cost** | Moderate, depends on quality | Low |
| **Availability** | Requires the `brotli` extra | Built-in, no extra needed |
| **`Content-Encoding` header** | `br` | `gzip` |
| **Quality range** | `0–11` | `1–9` |
| **Default quality** | `6` | `9` |
| **Enable via config** | `compression='brotli'` | `compression='gzip'` (default) |
| **Best for** | Large payloads where bandwidth matters | Minimal-dependency environments |

:::tip
For most workloads, the bandwidth savings from brotli outweigh the CPU costs. Install the `brotli` extra and pass `compression='brotli'` unless you can't install additional packages in your environment.
:::
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ dependencies = [
"pydantic[email]>=2.11.0",
]

[project.optional-dependencies]
brotli = ["brotli>=1.0.9"]

[project.urls]
"Apify Homepage" = "https://apify.com"
"Homepage" = "https://docs.apify.com/api/client/python/"
Expand Down
16 changes: 15 additions & 1 deletion src/apify_client/_apify_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,16 @@
WebhookDispatchCollectionClientAsync,
)
from apify_client._statistics import ClientStatistics
from apify_client._utils import check_custom_headers
from apify_client._utils.http import check_custom_headers
from apify_client.http_clients import HttpClient, HttpClientAsync, ImpitHttpClient, ImpitHttpClientAsync
from apify_client.http_compressors._resolve import resolve_compressor

if TYPE_CHECKING:
from datetime import timedelta

from apify_client.http_compressors._base import HttpCompressor
from apify_client.types import HttpCompressionAlgorithm


@docs_group('Apify API clients')
class ApifyClient:
Expand Down Expand Up @@ -122,6 +126,7 @@ def __init__(
timeout_long: timedelta = DEFAULT_TIMEOUT_LONG,
timeout_max: timedelta = DEFAULT_TIMEOUT_MAX,
headers: dict[str, str] | None = None,
compression: HttpCompressionAlgorithm | HttpCompressor = 'gzip',
) -> None:
"""Initialize the Apify API client.

Expand All @@ -143,6 +148,8 @@ def __init__(
timeout_long: Default timeout for long-duration API operations (long-polling, streaming, ...).
timeout_max: Maximum timeout cap for exponential timeout growth across retries.
headers: Additional HTTP headers to include in all API requests.
compression: Compression algorithm for request bodies. Pass a string literal to select an algorithm,
or an `HttpCompressor` instance for finer-grained control.
"""
# We need to do this because of mocking in tests and default mutable arguments.
api_url = DEFAULT_API_URL if api_url is None else api_url
Expand Down Expand Up @@ -205,6 +212,7 @@ def __init__(
self._timeout_long = timeout_long
self._timeout_max = timeout_max
self._headers = headers
self._http_compressor = resolve_compressor(compression)

@classmethod
def with_custom_http_client(
Expand Down Expand Up @@ -270,6 +278,7 @@ def http_client(self) -> HttpClient:
min_delay_between_retries=self._min_delay_between_retries,
statistics=self._statistics,
headers=self._headers,
http_compressor=self._http_compressor,
)

return self._http_client
Expand Down Expand Up @@ -476,6 +485,7 @@ def __init__(
timeout_long: timedelta = DEFAULT_TIMEOUT_LONG,
timeout_max: timedelta = DEFAULT_TIMEOUT_MAX,
headers: dict[str, str] | None = None,
compression: HttpCompressionAlgorithm | HttpCompressor = 'gzip',
) -> None:
"""Initialize the Apify API client.

Expand All @@ -497,6 +507,8 @@ def __init__(
timeout_long: Default timeout for long-duration API operations (long-polling, streaming, ...).
timeout_max: Maximum timeout cap for exponential timeout growth across retries.
headers: Additional HTTP headers to include in all API requests.
compression: Compression algorithm for request bodies. Pass a string literal to select an algorithm,
or an `HttpCompressor` instance for finer-grained control.
"""
# We need to do this because of mocking in tests and default mutable arguments.
api_url = DEFAULT_API_URL if api_url is None else api_url
Expand Down Expand Up @@ -559,6 +571,7 @@ def __init__(
self._timeout_long = timeout_long
self._timeout_max = timeout_max
self._headers = headers
self._http_compressor = resolve_compressor(compression)

@classmethod
def with_custom_http_client(
Expand Down Expand Up @@ -624,6 +637,7 @@ def http_client(self) -> HttpClientAsync:
min_delay_between_retries=self._min_delay_between_retries,
statistics=self._statistics,
headers=self._headers,
http_compressor=self._http_compressor,
)
return self._http_client

Expand Down
10 changes: 3 additions & 7 deletions src/apify_client/_resource_clients/_resource_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,9 @@
from apify_client._consts import DEFAULT_WAIT_FOR_FINISH, DEFAULT_WAIT_WHEN_JOB_NOT_EXIST
from apify_client._docs import docs_group
from apify_client._logging import WithLogDetailsClient
from apify_client._utils import (
catch_not_found_for_resource_or_throw,
catch_not_found_or_throw,
response_to_dict,
to_safe_id,
to_seconds,
)
from apify_client._utils.errors import catch_not_found_for_resource_or_throw, catch_not_found_or_throw
from apify_client._utils.http import response_to_dict, to_safe_id
from apify_client._utils.time import to_seconds
from apify_client.errors import ApifyApiError

if TYPE_CHECKING:
Expand Down
9 changes: 3 additions & 6 deletions src/apify_client/_resource_clients/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@
UpdateActorRequest,
)
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import (
encode_key_value_store_record_value,
encode_webhooks_to_base64,
response_to_dict,
to_seconds,
)
from apify_client._utils.encoding import encode_key_value_store_record_value, encode_webhooks_to_base64
from apify_client._utils.http import response_to_dict
from apify_client._utils.time import to_seconds

if TYPE_CHECKING:
from datetime import timedelta
Expand Down
2 changes: 1 addition & 1 deletion src/apify_client/_resource_clients/actor_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)
from apify_client._pagination import get_items_iterator, get_items_iterator_async
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import to_seconds
from apify_client._utils.time import to_seconds

if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterator
Expand Down
2 changes: 1 addition & 1 deletion src/apify_client/_resource_clients/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from apify_client._docs import docs_group
from apify_client._models import Build, BuildResponse
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import response_to_dict
from apify_client._utils.http import response_to_dict

if TYPE_CHECKING:
from datetime import timedelta
Expand Down
7 changes: 2 additions & 5 deletions src/apify_client/_resource_clients/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,8 @@
from apify_client._models import Dataset, DatasetResponse, DatasetStatistics, DatasetStatisticsResponse
from apify_client._pagination import DEFAULT_CHUNK_SIZE, get_items_iterator, get_items_iterator_async
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import (
create_storage_content_signature,
response_to_dict,
response_to_list,
)
from apify_client._utils.crypto import create_storage_content_signature
from apify_client._utils.http import response_to_dict, response_to_list

if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterator
Expand Down
11 changes: 4 additions & 7 deletions src/apify_client/_resource_clients/key_value_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,10 @@
)
from apify_client._pagination import DEFAULT_CHUNK_SIZE, get_cursor_iterator, get_cursor_iterator_async
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import (
catch_not_found_or_throw,
create_hmac_signature,
create_storage_content_signature,
encode_key_value_store_record_value,
response_to_dict,
)
from apify_client._utils.crypto import create_hmac_signature, create_storage_content_signature
from apify_client._utils.encoding import encode_key_value_store_record_value
from apify_client._utils.errors import catch_not_found_or_throw
from apify_client._utils.http import response_to_dict
from apify_client.errors import ApifyApiError, InvalidResponseBodyError

if TYPE_CHECKING:
Expand Down
2 changes: 1 addition & 1 deletion src/apify_client/_resource_clients/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from apify_client._docs import docs_group
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import catch_not_found_for_resource_or_throw
from apify_client._utils.errors import catch_not_found_for_resource_or_throw
from apify_client.errors import ApifyApiError

if TYPE_CHECKING:
Expand Down
4 changes: 3 additions & 1 deletion src/apify_client/_resource_clients/request_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
)
from apify_client._pagination import DEFAULT_CHUNK_SIZE, get_cursor_iterator, get_cursor_iterator_async
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import catch_not_found_or_throw, response_to_dict, to_seconds
from apify_client._utils.errors import catch_not_found_or_throw
from apify_client._utils.http import response_to_dict
from apify_client._utils.time import to_seconds
from apify_client.errors import ApifyApiError

if TYPE_CHECKING:
Expand Down
4 changes: 3 additions & 1 deletion src/apify_client/_resource_clients/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._status_message_watcher import StatusMessageWatcher, StatusMessageWatcherAsync
from apify_client._streamed_log import StreamedLog, StreamedLogAsync
from apify_client._utils import encode_key_value_store_record_value, response_to_dict, to_safe_id, to_seconds
from apify_client._utils.encoding import encode_key_value_store_record_value
from apify_client._utils.http import response_to_dict, to_safe_id
from apify_client._utils.time import to_seconds

if TYPE_CHECKING:
import logging
Expand Down
2 changes: 1 addition & 1 deletion src/apify_client/_resource_clients/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
ScheduleResponse,
)
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import response_to_dict
from apify_client._utils.http import response_to_dict

if TYPE_CHECKING:
from apify_client.types import Timeout
Expand Down
4 changes: 3 additions & 1 deletion src/apify_client/_resource_clients/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
UpdateTaskRequest,
)
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import encode_webhooks_to_base64, response_to_dict, to_seconds
from apify_client._utils.encoding import encode_webhooks_to_base64
from apify_client._utils.http import response_to_dict
from apify_client._utils.time import to_seconds

if TYPE_CHECKING:
from datetime import timedelta
Expand Down
2 changes: 1 addition & 1 deletion src/apify_client/_resource_clients/task_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)
from apify_client._pagination import get_items_iterator, get_items_iterator_async
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import to_seconds
from apify_client._utils.time import to_seconds

if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterator
Expand Down
2 changes: 1 addition & 1 deletion src/apify_client/_resource_clients/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
UserPublicInfo,
)
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import response_to_dict
from apify_client._utils.http import response_to_dict

if TYPE_CHECKING:
from apify_client.types import Timeout
Expand Down
2 changes: 1 addition & 1 deletion src/apify_client/_resource_clients/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
WebhookUpdate,
)
from apify_client._resource_clients._resource_client import ResourceClient, ResourceClientAsync
from apify_client._utils import response_to_dict
from apify_client._utils.http import response_to_dict

if TYPE_CHECKING:
from apify_client._literals import WebhookEventType
Expand Down
2 changes: 1 addition & 1 deletion src/apify_client/_status_message_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import TYPE_CHECKING, Self

from apify_client._docs import docs_group
from apify_client._utils import to_seconds
from apify_client._utils.time import to_seconds

if TYPE_CHECKING:
import logging
Expand Down
Loading
Loading