Skip to content

feat: add zstd request body compression - #1218

Open
apify-factory wants to merge 21 commits into
masterfrom
sync/0e1932b28308
Open

apify-factory wants to merge 21 commits into
masterfrom
sync/0e1932b28308

Conversation

@apify-factory

@apify-factory apify-factory commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Reference change

apify/apify-client-js#1200

Ported change

  • HttpCompressionAlgorithm (apify_client.types) now accepts 'zstd', and resolve_compressor maps it to the new compressor. The compression argument of ApifyClient / ApifyClientAsync and of ActorServerClient.request therefore accepts 'zstd'.
  • New public apify_client.http_compressors.ZstdHttpCompressor (http_compressors/_zstd.py), modeled on the gzip and brotli compressors. It reports content_encoding = 'zstd' and takes a keyword-only quality from 1 to 19, defaulting to 3. Levels 20–22 are rejected because their window exceeds the 8 MB that RFC 9659 requires HTTP decoders to support. The body-selection rules (minimum size, already-compressed content types, caller-set Content-Encoding) are the same as for the other algorithms.
  • Python-specific differences from the reference:
    • Source of zstd. Python 3.14+ uses the standard library's compression.zstd. Older versions use the official backports.zstd package through a new optional zstd extra (pip install "apify-client[zstd]"), following the pattern of the brotli extra.
    • Missing zstd. If the extra is missing, or a 3.14+ Python was built without zstd, import apify_client still works. Only accessing ZstdHttpCompressor or passing compression='zstd' raises a clear ImportError.
    • Supporting changes. try_import now treats a missing parent package of a dotted dependency name as the dependency being missing, and accepts extra_name=None for dependencies no extra can install. [tool.ty.analysis] allowed-unresolved-imports lists backports.zstd, because ty checks against Python 3.11 even in 3.14+ environments, where the package is not installed.
    • Invalid quality. An invalid quality raises ValueError, like the existing Python compressors, rather than the JS ArgumentValidationError.

@github-actions github-actions Bot added the t-tooling Issues with this label are in the ownership of the tooling team. label Nov 10, 2026
@codecov

codecov Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.25000% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.66%. Comparing base (eb8ded4) to head (fd96b88).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
src/apify_client/http_compressors/_zstd.py 75.00% 7 Missing ⚠️
src/apify_client/http_compressors/__init__.py 90.90% 1 Missing ⚠️
src/apify_client/types.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1218      +/-   ##
==========================================
- Coverage   95.76%   95.66%   -0.10%     
==========================================
  Files          63       64       +1     
  Lines        6676     6717      +41     
==========================================
+ Hits         6393     6426      +33     
- Misses        283      291       +8     
Flag Coverage Δ
integration 90.63% <58.33%> (-0.04%) ⬇️
unit 91.00% <81.25%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Pijukatel
Pijukatel requested a review from vdusek November 10, 2026 10:17

@vdusek vdusek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a self-review / staff review to the porting process?

Comment thread pyproject.toml Resolved
Comment thread docs/01_introduction/index.mdx Resolved
Comment thread README.md Resolved
@Pijukatel
Pijukatel requested a review from vdusek November 10, 2026 14:57

@vdusek vdusek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: compressor_case in tests/unit/test_http_clients.py (outside the diff) still covers only gzip and brotli. Consider adding a zstd case: pytest.param((ZstdHttpCompressor(), 'zstd', zstd.decompress), id='zstd').

✍️ Drafted by Claude Code

else:
# Not `from backports import zstd`: when another `backports.*` package is installed but this one is not, that
# raises a plain `ImportError` instead of the `ModuleNotFoundError` the optional-extra handling expects.
import backports.zstd as zstd # noqa: PLR0402

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: If backports.zstd is installed but its C extension fails to load, this raises a plain ImportError. try_import catches only ModuleNotFoundError, so import apify_client fails for every user, including gzip users. The 3.14+ branch above handles this case. Should this branch handle it the same way, or is it fine because brotli behaves the same?

✍️ Drafted by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, handled in b114508 and db6ed4e. On Python < 3.14, a plain ImportError from backports.zstd is now re-raised as ModuleNotFoundError(name='backports.zstd'), so import apify_client keeps working and only ZstdHttpCompressor raises. The error says the package is installed but cannot be loaded, and suggests pip install --force-reinstall backports.zstd or gzip/brotli. try_import no longer appends the "install the extra" hint to an error raised from another one, since installing the extra again doesn't help in that case. Both cases have unit tests (test_zstd_import_hook_raises_clear_error_when_backport_fails_to_load, test_try_import_keeps_the_message_of_a_dependency_that_fails_to_load). brotli is left as it is.

Comment thread tests/unit/test_http_compressors.py Outdated
if sys.version_info >= (3, 14):
from compression import zstd
else:
import backports.zstd as zstd # noqa: PLR0402 - the same import form as `http_compressors/_zstd.py`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The reason for this import form in _zstd.py is to get a ModuleNotFoundError that try_import can catch. Tests don't go through try_import, so the plain form works and the noqa can go.

Suggested change
import backports.zstd as zstd # noqa: PLR0402 - the same import form as `http_compressors/_zstd.py`
from backports import zstd

✍️ Drafted by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this in 345e092 but reverted it in 7964ffa. Type checking runs with python-version = "3.11", so ty checks this branch even on 3.14/3.15, where backports.zstd isn't installed. allowed-unresolved-imports = ["backports.zstd"] covers import backports.zstd. from backports import zstd resolves the module backports, so it fails with Cannot resolve imported module 'backports' and would break the 3.14 and 3.15 type_check jobs. Allowing all of backports would also work, but it's broader. I kept the dotted form and made the noqa reason say why.

Comment thread tests/unit/test_key_value_store.py Outdated
if sys.version_info >= (3, 14):
from compression import zstd
else:
import backports.zstd as zstd # noqa: PLR0402 - the same import form as `http_compressors/_zstd.py`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The reason for this import form in _zstd.py is to get a ModuleNotFoundError that try_import can catch. Tests don't go through try_import, so the plain form works and the noqa can go.

Suggested change
import backports.zstd as zstd # noqa: PLR0402 - the same import form as `http_compressors/_zstd.py`
from backports import zstd

✍️ Drafted by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in test_http_compressors.py: I kept import backports.zstd as zstd, because from backports import zstd fails ty on 3.14+ (it resolves backports, which the allowed-unresolved-imports entry doesn't cover). The noqa now gives the reason.

Comment thread README.md Outdated
```

To compress with `zstd`, pass `compression='zstd'`. Python 3.14 and newer have it in the standard library. On older
versions, install the optional `zstd` extra: `pip install "apify-client[zstd]"`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The brotli section above shows both pip install and uv add. Show uv add "apify-client[zstd]" here too. The same applies to docs/01_introduction/index.mdx.

✍️ Drafted by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added uv add "apify-client[zstd]" here and in docs/01_introduction/index.mdx in 7fe7fe1.

@@ -14,9 +14,14 @@

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add integration test that sends a zstd body to the live API?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not added in this PR: I can't run tests against the live API from here, and I don't want to add an integration test that has never been run. The natural place is next to test_key_value_store_set_and_get_pre_compressed_record in tests/integration/test_key_value_store.py: a client with compression='zstd' pushing a large record or dataset items and reading them back. It needs someone with API tokens to add and run it, either in this PR or as a follow-up.

@apify-factory

Copy link
Copy Markdown
Contributor Author

Re the review note on compressor_case in tests/unit/test_http_clients.py: added the zstd case in 345e092.

Cover a key-value store record and a dataset push through a client
that compresses request bodies with zstd, for both transports and
both the sync and async client.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ocuAK2fAevKdoyPCB2HvC

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-client-sync t-tooling Issues with this label are in the ownership of the tooling team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants