Skip to content

feat: Add transport-level compression, remove application-level is_compressed#210

Open
tomaz-lc wants to merge 8 commits intocli-v2from
feat/transport-compression
Open

feat: Add transport-level compression, remove application-level is_compressed#210
tomaz-lc wants to merge 8 commits intocli-v2from
feat/transport-compression

Conversation

@tomaz-lc
Copy link
Contributor

@tomaz-lc tomaz-lc commented Feb 23, 2026

Summary

  • Add Accept-Encoding: zstd, gzip, deflate header to all API requests for transparent HTTP transport-level compression
  • Improve data field compression handling - endpoints that used is_compressed=true (base64-encoded gzip inside JSON) now return plain JSON fields, with compression handled transparently at the transport layer instead
  • New transport_compression module handles Accept-Encoding negotiation and Content-Encoding decompression (zstd, gzip, deflate)
  • zstandard added as a hard dependency with runtime graceful fallback - pre-built wheels available for all major platforms, but if unavailable the SDK still works with gzip/deflate only
  • Client.unwrap() deprecated but kept for backward compatibility

Why

The previous is_compressed=true approach gzip-compressed individual data fields and base64-encoded them inside JSON responses (base64(gzip(data))). This required explicit client-side decompression (unwrap()) and prevented efficient transport-level compression since compressing already-compressed data is wasteful.

Transport-level compression is better in every way - it's transparent to the caller, handled by standard HTTP content negotiation, covers the entire response body (not just specific fields), and supports modern algorithms like zstd which offer better ratios and faster decompression than gzip.

Backward compatibility

This change is fully backward compatible:

  • The API server continues to support is_compressed=true for older clients that haven't upgraded yet
  • Client.unwrap() is deprecated but still functional for any external callers using it directly
  • Uncompressed responses (no Content-Encoding header) pass through unchanged
  • No breaking changes to any public API surface - all existing method signatures and return types are preserved

There is a companion API-side PR that adds transport compression support on the server. Old clients sending is_compressed=true will continue to work as before - the server skips transport compression for those requests to avoid double compression.

zstandard compatibility

zstandard is listed as a hard dependency in pyproject.toml so pip installs it automatically. It ships pre-built wheels for all major platforms - no C compiler or build toolchain needed at install time.

Platform Architecture Python 3.10 Python 3.11 Python 3.12 Python 3.13
Linux (glibc) x86_64 wheel wheel wheel wheel
Linux (glibc) aarch64 wheel wheel wheel wheel
Linux (musl) x86_64 wheel wheel wheel wheel
Linux (musl) aarch64 wheel wheel wheel wheel
macOS x86_64 wheel wheel wheel wheel
macOS ARM64 (Apple Silicon) wheel wheel wheel wheel
Windows x86_64 wheel wheel wheel wheel
Windows ARM64 wheel wheel wheel wheel

Runtime fallback: If zstandard can't be imported (exotic platform without a wheel or C compiler), the SDK still works - Accept-Encoding falls back to "gzip, deflate" and all transport decompression continues to work via stdlib zlib. The zstd code path is guarded with try/except ImportError at module load time.

Changes

File Change
pyproject.toml Add zstandard>=0.22.0 as hard dependency
limacharlie/transport_compression.py New - compression negotiation + decompression with zstd fallback
limacharlie/client.py Add Accept-Encoding header, transport decompression, deprecate unwrap()
limacharlie/sdk/sensor.py Remove is_compressed + unwrap() from get_events, get_children_events
limacharlie/sdk/organization.py Remove is_compressed + unwrap() from get_detections, get_audit_logs, get_jobs
limacharlie/config.py Fix os.chown crash on Windows (Unix-only API)
.github/workflows/ci.yml New - cross-platform CI (ubuntu/macos/windows x Python 3.10-3.13)
tests/unit/test_transport_compression.py New - compression module tests including zstd-unavailable fallback
tests/unit/test_client.py Transport compression integration tests
tests/unit/test_config.py Fix permission assertion for Windows (NTFS uses ACLs, not Unix mode bits)
tests/unit/test_sdk_sensor.py Updated for new response format
tests/unit/test_sdk_organization.py Updated for new response format

Additional fixes

  • Windows compatibility in config.py: Guard os.chown() with hasattr(os, "chown") - os.chown and os.getuid don't exist on Windows
  • Windows test fix in test_config.py: os.chmod(0o600) doesn't enforce Unix-style permissions on NTFS - skip permission assertion on Windows
  • New GHA CI workflows: Matrix of ubuntu/macos/windows x Python 3.10/3.11/3.12/3.13 - runs unit tests, wheel install sanity checks, zstd verification, and a dedicated no-zstd fallback job

Test plan

  • Cross-platform CI passes (ubuntu/macos/windows x Python 3.10-3.13)
  • Verify Accept-Encoding: zstd, gzip, deflate sent on all requests
  • Verify zstd decompression round-trip works end-to-end
  • No-zstd fallback CI job passes (zstandard uninstalled, SDK works with gzip/deflate)
  • Integration test with transport compression enabled server-side
  • Verify unwrap() still works for external callers

🤖 Generated with Claude Code

…mpressed

Add Accept-Encoding (zstd, gzip, deflate) to all API requests so the server
can compress response bodies at the HTTP transport layer. This replaces the
application-level is_compressed=true query param which caused base64(gzip(data))
encoding inside JSON fields.

- New transport_compression module handles Accept-Encoding negotiation and
  Content-Encoding decompression (zstd via optional zstandard package, gzip, deflate)
- Client._rest_call() sends Accept-Encoding on every request and decompresses
  response bodies (including error responses) before JSON parsing
- Remove is_compressed=true from 5 SDK callsites (get_events, get_children_events,
  get_detections, get_audit_logs, get_jobs) and replace unwrap() calls with
  direct JSON field access
- Deprecate Client.unwrap() (kept for external backward compat)
- zstandard is an optional dependency: pip install limacharlie[zstd]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tomaz-lc tomaz-lc requested a review from maximelb February 23, 2026 09:21
tomaz-lc and others added 7 commits February 23, 2026 10:21
Move zstandard from optional to required dependency - pre-built wheels
are available for all supported platforms (Linux/macOS/Windows, x86_64/ARM64,
glibc/musl).

Add GitHub Actions CI workflow that runs unit tests and wheel install
verification across Python 3.10-3.13 on ubuntu, macos, and windows
runners. Each job verifies zstandard installs and zstd decompression
works end-to-end.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
os.chown and os.getuid are Unix-only. Guard with hasattr check so
config file writing works on Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- limacharlie version -> limacharlie --version (correct CLI flag)
- Use pip install --find-links instead of shell glob for wheel install
  (glob doesn't work on Windows PowerShell)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Windows NTFS uses ACLs, not Unix permission bits. os.chmod(0o600)
doesn't restrict access the same way on Windows, so the 0o777 mask
assertion fails. Only assert file permissions on Unix platforms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
zstandard is kept as a hard dependency (pre-built wheels for all major
platforms), but the runtime now handles ImportError gracefully. If zstd
can't be imported (exotic platform, no C compiler), Accept-Encoding
falls back to "gzip, deflate" and zstd-encoded responses pass through
as-is. This prevents pip install failures from making the entire SDK
unusable.

- Restore try/except ImportError guard in transport_compression.py
- Add _HAS_ZSTD flag for runtime feature detection
- Add tests for fallback path (mock zstandard away, verify behavior)
- Add no-zstd-fallback CI job that uninstalls zstandard and verifies
  the SDK still works with gzip/deflate only

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract inline python -c snippets from ci.yml into standalone scripts
under scripts/ci/ for readability and easier local debugging.

Add edge case tests for transport_compression:
- Raw deflate (no zlib header) vs zlib-wrapped deflate
- Case insensitivity for all encodings (zstd, gzip, deflate)
- Whitespace around Content-Encoding header values
- Empty data with/without encoding
- _HAS_ZSTD flag assertion
- zstd passthrough case insensitivity when lib unavailable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Top-level `import zstandard` in test_transport_compression.py caused
pytest collection to fail when zstandard was uninstalled (no-zstd CI
job). Tests that need zstandard now use local imports with
pytest.mark.skipif(_HAS_ZSTD) or pytest.importorskip(). Tests that
verify the no-zstd fallback run in both environments.

- With zstandard: 50 passed, 2 skipped (no-zstd-only tests)
- Without zstandard: 44 passed, 8 skipped (zstd-specific tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tomaz-lc tomaz-lc marked this pull request as ready for review February 23, 2026 11:08
@tomaz-lc tomaz-lc requested a review from dzimine-lc February 23, 2026 11:14
@maximelb
Copy link
Contributor

Let's add it to the new SDK/CLI that way we can focus our efrorts on what we need to test anyway.

Copy link
Contributor

@dzimine-lc dzimine-lc left a comment

Choose a reason for hiding this comment

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

@tomaz-lc the change is good, but it also mixes the compression change with intro of GH action based CI, I recommend we separate the two, and disuss/design/do CI in a diff PR?


on:
pull_request:
branches: [cli-v2, master]
Copy link
Contributor

Choose a reason for hiding this comment

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

I am -1 on any backward support to old python, we leave it on v1 (v3 to be precise), propose we don't carry any backward compatibiltiy stuff to cli-v2

Suggest you modify it to 1) only act on cli-v2 branch and remove all backward compatibility part - matrix-python-version.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't aware until very recently that cli-v2 branch will be long running for a while.

Having said that, since this branch is already targeted towards cli-v2 it should work fine. Once it's merged / takes over the old master, it should work just fine.

In short - it won't run against old version, just against the new one.

@tomaz-lc
Copy link
Contributor Author

@dzimine-lc

@tomaz-lc the change is good, but it also mixes the compression change with intro of GH action based CI, I recommend we separate the two, and disuss/design/do CI in a diff PR?

I can split it, yeah, but I needed it to test the changes in this branch + it also detected some existing Windows related bugs :)

Will split CI into a separate branch targeted towards this branch. So the flow is / will be cli-v2 <- this branch <- new branch with CI changes

@dzimine-lc
Copy link
Contributor

consider doing CI changes first.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants