Skip to content

feat(py-client): Implement "many" api for batch requests - #546

Open
matt-codecov wants to merge 1 commit into
mainfrom
matt/py-client-many
Open

matt-codecov wants to merge 1 commit into
mainfrom
matt/py-client-many

Conversation

@matt-codecov

@matt-codecov matt-codecov commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Changelog Entry

  • Add Session.many() to run multiple get, head, put and delete operations concurrently, batching small ones into batch requests

Details

#277 / #478 implemented the many API in the Rust client which uses Objectstore's batch endpoint. This PR ports it over to Python.

Incorporates direction from #419.

Closes FS-330

Some notes:

  • Hand-rolls the max concurrency limit rather than rely on ThreadPoolExecutor's max_workers to allow for results to be streamed from individual batch requests without having to buffer the whole response. Otherwise a batch of 1000 5MB GET results would eat 5GB RAM and delay yielding anything to the caller.
  • Kind of aggressive about raising errors. Being flexible is the server's job, not the client's.
  • There's duplicated code. The Get / Put operation type classes copy the arg list of the get() and put() methods on session, the decompression code in get() is copied... but I didn't want to touch existing code much to reorganize in this PR.
  • Default concurrency is 1 because that's the default urllib3 connection pool size. You can still send concurrent requests with a connection pool size of 1, it just opens/closes a connection per request and logs a warning about it instead of actually pooling.
  • Actually reads the "part number" header from the batch endpoint response. Each operation's response is tagged with the operation's index in the input list so you can figure out which keyless PUT was assigned which key.
  • Robot generated the tests, haven't reviewed them yet.

@linear-code

linear-code Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

FS-361

FS-330

@codecov

codecov Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.93443% with 37 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.07%. Comparing base (c555e65) to head (c612923).

Files with missing lines Patch % Lines
clients/python/src/objectstore_client/many.py 92.50% 36 Missing ⚠️
clients/python/src/objectstore_client/client.py 90.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #546      +/-   ##
==========================================
+ Coverage   92.02%   92.07%   +0.04%     
==========================================
  Files         118      120       +2     
  Lines       24332    24931     +599     
==========================================
+ Hits        22391    22954     +563     
- Misses       1941     1977      +36     
Components Coverage Δ
Rust Backend 94.95% <ø> (ø)
Rust Client 84.29% <ø> (ø)
Python Client 93.92% <93.93%> (+0.03%) ⬆️

☔ 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.

@jan-auer

jan-auer commented Jul 9, 2026 •

Copy link
Copy Markdown
Member

Please see also the now closed #419. It's gone out of sync with main, so it's better to carry on here.

The prior PR focused on streaming as much as possible and had gone through a round of feedback with the Python SDK maintainers for the public API. Also, there's configuration for concurrency that uses an optional thread pool executor. I hope there are some parts we could leverage from that.

@matt-codecov

Copy link
Copy Markdown
Contributor Author

bugbot run
@sentry review

Comment thread clients/python/src/objectstore_client/many.py Outdated
Comment thread clients/python/src/objectstore_client/many.py Outdated
Comment thread clients/python/src/objectstore_client/many.py Outdated
@matt-codecov
matt-codecov marked this pull request as ready for review August 25, 2026 03:00
@matt-codecov
matt-codecov requested a review from a team as a code owner August 25, 2026 03:00
Comment thread clients/python/src/objectstore_client/client.py Outdated
Comment thread clients/python/src/objectstore_client/many.py Outdated
Comment thread clients/python/README.md Outdated
@lcian lcian changed the title feat(py-client): implement "many" api for batch requests feat(py-client): Implement "many" api for batch requests Aug 25, 2026
Comment thread clients/python/src/objectstore_client/client.py Outdated
Comment thread clients/python/src/objectstore_client/many.py Outdated
Comment thread clients/python/src/objectstore_client/many.py Outdated

A port of the ``ZSTD_COMPRESSBOUND`` macro definition in ``zstd.h``.
"""
margin = ((128 << 10) - size) >> 11 if size < (128 << 10) else 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I assume this is correct.

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.

https://github.com/facebook/zstd/blob/82d322c4973d9e2968d94047a40892bc6d9a9bdf/lib/zstd.h#L249 is the source. it is a bit of a mess to parse but here it is with some extra indentation for clarity:

#define ZSTD_COMPRESSBOUND(srcSize) \
  ( \
    ((size_t)(srcSize) >= ZSTD_MAX_INPUT_SIZE) \
      ? 0 \
      : (srcSize) + ((srcSize)>>8) + (         /* size + (size >> 8) +             */ \
        ((srcSize) < (128<<10))                /* if size < (128 << 10)            */ \
          ? (((128<<10) - (srcSize)) >> 11)    /* then ((128 << 10) - size) >> 11  */ \
          : 0                                  /* else 0                           */ \
      ) \
  )

once upon a time writing convoluted preprocessor macros was my job

Comment thread clients/python/src/objectstore_client/many.py Outdated
Comment on lines +466 to +470
if concurrency == 1 and executor is None:
for item in work:
yield from _run_work(session, item)
else:
yield from _execute_concurrent(session, work, concurrency, executor)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we really need the special case for concurrency=1 or could we just send the ops into _execute_concurrent with a special/single-threaded executor and that's it?

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.

the special case here skips any executor/threading and just runs it on the caller thread. i don't think there is a "same thread executor" ready-made we could use here. but it probably wouldn't be hard to write

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 left this as-is

Comment thread clients/python/src/objectstore_client/many.py Outdated
Comment thread clients/python/src/objectstore_client/many.py
Comment thread clients/python/src/objectstore_client/many.py Outdated
Comment thread clients/python/src/objectstore_client/many.py
Comment thread clients/python/src/objectstore_client/formdata.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ff03dcd. Configure here.

Comment thread clients/python/src/objectstore_client/many.py Outdated
Comment thread clients/python/src/objectstore_client/many.py Outdated
Comment thread clients/python/src/objectstore_client/many.py Outdated
@lcian
lcian self-requested a review September 4, 2026 11:43
Comment thread clients/python/src/objectstore_client/many.py Outdated
@lcian
lcian requested review from lcian and removed request for lcian September 25, 2026 10:26
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


Breaking Changes 🛠

Py Client

  • Split the signing key from the token policy: SecretKey(kid, secret_key) now only holds the EdDSA key, and TokenGenerator(key, expiry_seconds=60, permissions=None) wraps it with the token defaults and is what you pass to Client(token=...). The deprecated sign_for_scope() alias is removed. by sentry-junior in #721
    • Replace SecretKey(kid, secret_key, expiry_seconds, permissions) or TokenGenerator(kid, secret_key, ...) with TokenGenerator(SecretKey(kid, secret_key), expiry_seconds, permissions).
      # Before
      client = Client(url, token=SecretKey(kid, secret_key, expiry_seconds=300))
      
      # After
      key = SecretKey(kid, secret_key)
      client = Client(url, token=TokenGenerator(key, expiry_seconds=300))
    • Replace .sign_for_scope(...) with .token_for_scope(...).
  • Remove the deprecated compression argument from Session.put() by sentry-junior in #718
    • Replace session.put(..., compression=...) with session.put(..., compress=...).
  • Remove the deprecated propagate_traces argument from Client by sentry-junior in #714
    • Drop propagate_traces=... from Client(...) calls. Sentry's StdlibIntegration, enabled by default, already propagates traces on Objectstore requests.

Rs Client

  • Remove the deprecated Session::mint_token() by sentry-junior in #717
    • Replace session.mint_token() with session.get_token().
  • Remove the deprecated TokenGenerator::sign() by sentry-junior in #716
    • Replace generator.sign(&scope) with generator.create_token(&scope).sign().
  • Remove the deprecated PutBuilder::compression() by sentry-junior in #715
    • Replace .compression(...) with .compress(...) on put builders.
  • Usecase::new now returns a Result, and the panicking constructor is now Usecase::from_static by sentry-junior in #710
    • Replace Usecase::new("name") with Usecase::from_static("name").
    • Replace Usecase::try_new(name) with Usecase::new(name).
  • Mark Error as #[non_exhaustive] so new error variants can be added without a breaking change by lcian in #706
    • Add a wildcard arm (_ => ...) to exhaustive match expressions on objectstore_client::Error.
  • Remove the multipart upload API (Session::initiate_multipart_upload(), Session::resume_multipart_upload()) and the multipart feature flag by lcian in #704
    • Use resumable uploads instead.
    • Remove match arms for the removed Error::InvalidPartNumber, Error::InvalidUploadId and Error::MultipartComplete variants.

New Features ✨

Py Client

  • Add Session.many() to run multiple get, head, put and delete operations concurrently, batching small ones into batch requests by matt-codecov in #546
  • Add a response_content_disposition option ("inline" or "attachment") to Session.object_url() and Session.presigned_object_url(). Pre-signed URLs include it in their signature. by jan-auer in #696
  • Add automatic resumable uploads by lcian in #667
  • Add private resumable upload API by lcian in #665

Rust Client

  • Surface upload offset mismatch as an error by lcian in #654
  • Add streaming versions of put_chunk by lcian in #649
  • Add Resumable Uploads (low-level) API by lcian in #622

Other

  • (clients) Add lifetime extensions for Python and Rust by jan-auer in #639
  • (local-fs) Support resumable uploads by lcian in #640
  • (resumable) Define and expose upload granularity by lcian in #652

Bug Fixes 🐛

  • (py-client) Honor zero retries by lcian in #679
  • (rust-client) Drain discarded response bodies by lcian in #629
  • Validate usecase names by jan-auer in #694
  • Update TTL durations on expiry extensions by jan-auer in #646

Internal Changes 🔧

  • (rust) Enable resumable uploads on docs.rs by jan-auer in #684
  • (rust-client) Use typed queries for resumable API by lcian in #628
  • Introduce a validated usecase name type by jan-auer in #695

Other

  • release: 0.2.10 by getsentry-bot in 52c2af31
  • release: 0.2.9 by getsentry-bot in 57511c28
  • release: 0.2.8 by getsentry-bot in ea62e342
  • release: 0.2.7 by getsentry-bot in 2f73fa50
  • release: 0.2.6 by getsentry-bot in 53706699

🤖 This preview updates automatically when you update the PR.

Co-Authored-By: Jan Michael Auer <1433023+jan-auer@users.noreply.github.com>
@sentry-junior
sentry-junior Bot force-pushed the matt/py-client-many branch from 9b05797 to c612923 Compare October 27, 2026 11:29
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