Skip to content

feat(client): Add nemoclient error hierarchy and fix streaming - #539

Merged
matthewgrossman merged 4 commits into
mainfrom
mgrossman/GENERIC1-aircore-827-migrate-first-plugin-to-nemoclient-typed-http-client-files-
Jul 1, 2026
Merged

feat(client): Add nemoclient error hierarchy and fix streaming#539
matthewgrossman merged 4 commits into
mainfrom
mgrossman/GENERIC1-aircore-827-migrate-first-plugin-to-nemoclient-typed-http-client-files-

Conversation

@matthewgrossman

@matthewgrossman matthewgrossman commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Foundational improvements to the NemoClient typed HTTP client, extracted from the Files migration PR (#429) to keep that PR focused on Files-specific changes.

Error hierarchy (errors.py — new)

  • Moved NemoHTTPError out of response.py into its own module
  • Added status-code-specific subclasses: BadRequestError (400), AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404), ConflictError (409), UnprocessableEntityError (422), RateLimitError (429), InternalServerError (500+)
  • Added raise_for_status() function that maps status codes to the correct subclass
  • All error paths (JSON, binary, stream, paginated) now raise NemoHTTPError subclasses consistently — previously binary/stream paths raised httpx.HTTPStatusError

send() raises on non-2xx (client.py)

  • send() now calls raise_for_status(raw) before parsing the body, so callers don't need .data() to trigger errors
  • .data() simplified to a plain body accessor
  • Removed _apply_client_options() and _raise_for_status()exist_ok infrastructure remains in PreparedRequest.client_options but is not acted on until the server returns entities on 409 (AIRCORE-866)

Binary/stream response API (response.py)

  • Replaced __enter__/__exit__ context manager protocol with simpler read() + stream() API
  • read() — opens the stream, reads all bytes, closes (one-shot); reads body before raise_for_status() so error detail is available
  • stream()@contextmanager yielding Iterator[bytes] (binary) or Iterator[ModelT] (stream)
  • No more _response state or http_response property
  • SSE support: auto-detects Content-Type: text/event-stream and strips data: prefixes before JSON parsing; skips non-data SSE fields (event:, id:, comments). Shared _parse_stream_line() helper used by both sync and async paths.

Other improvements

  • default_headers on BaseNemoClient — merged into every request; client_from_platform() propagates _custom_headers
  • JSON query param serialization — dict/list values are json.dumps()-ed instead of Python repr()
  • exclude_unset=True on body serialization — partial update bodies only send explicitly set fields

Test plan

  • 54 client tests pass (packages/nemo_platform_plugin/tests/client/)
  • 25 example-plugin tests pass (plugins/example-plugin/tests/test_sdk.py), including new coverage for:
    • Binary upload/download via read() and stream()
    • NDJSON streaming via stream()
    • SSE framing (data: prefix stripping, non-data field skipping, sync + async)
    • Error detail extraction from binary error responses

🤖 Generated with Claude Code

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman
matthewgrossman requested review from a team as code owners July 1, 2026 19:43
@github-actions github-actions Bot added the feat label Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds shared HTTP error handling, default headers, query serialization, and streaming response wrappers. Client and SDK tests now assert raised errors and the new sync/async streaming APIs.

Changes

Client error handling, request wiring, and streaming refactor

Layer / File(s) Summary
HTTP error hierarchy
packages/nemo_platform_plugin/src/nemo_platform_plugin/client/errors.py
Adds NemoHTTPError, status-specific subclasses, and raise_for_status for non-2xx responses.
Client request wiring
packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py, packages/nemo_platform_plugin/src/nemo_platform_plugin/client/adapter.py, packages/nemo_platform_plugin/src/nemo_platform_plugin/client/endpoint.py, packages/nemo_platform_plugin/src/nemo_platform_plugin/client/types.py
Adds default_headers, merges them into requests, serializes dict/list query params, forwards platform headers, excludes unset body fields, and raises before typed JSON validation.
Response streaming refactor
packages/nemo_platform_plugin/src/nemo_platform_plugin/client/response.py
Moves binary and stream handling to stream() context managers and routes pagination through shared error handling.
Client tests
packages/nemo_platform_plugin/tests/client/test_client.py, packages/nemo_platform_plugin/tests/client/test_client_options.py
Updates assertions to raised NemoHTTPError subclasses and adds coverage for query serialization and retry behavior.
Example SDK streaming tests
plugins/example-plugin/tests/test_sdk.py
Adds mocked sync/async streaming helpers plus binary and line-streaming coverage.

Possibly related PRs

Suggested labels: fix

Suggested reviewers: maxdubrinsky, mckornfield

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main changes: a new NemoClient error hierarchy and streaming behavior updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mgrossman/GENERIC1-aircore-827-migrate-first-plugin-to-nemoclient-typed-http-client-files-

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (3)
plugins/example-plugin/tests/test_sdk.py (2)

247-301: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing async download_blob stream test.

Sync stream is tested (test_sync_download_blob_stream), async isn't. Add the async counterpart for parity.

✅ Suggested test
`@pytest.mark.asyncio`
async def test_async_download_blob_stream() -> None:
    client, mock_http = _async_client()
    raw = httpx.Response(200, content=b"chunk1chunk2", request=httpx.Request("GET", BASE))
    mock_http.stream = _async_stream_ctx(raw)

    resp = await client.download_blob(name="pic.png")
    async with resp.stream() as chunks:
        result = b"".join([c async for c in chunks])

    assert result == b"chunk1chunk2"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/example-plugin/tests/test_sdk.py` around lines 247 - 301, Add the
missing async parity test for blob streaming in the existing test module by
mirroring test_sync_download_blob_stream with a new
test_async_download_blob_stream. Use _async_client, set mock_http.stream with
_async_stream_ctx, call client.download_blob(name="pic.png"), and verify the
streamed chunks join to the expected bytes inside an async with resp.stream()
block.

247-336: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No error-path test for streaming/binary responses.

read()/stream() call raise_for_status internally (response.py), but no test here exercises a non-2xx status for download_blob or count. Worth adding to lock in the new error hierarchy on these paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/example-plugin/tests/test_sdk.py` around lines 247 - 336, The
streaming/binary response tests for download_blob and count only cover success
cases, so add error-path coverage to lock in the new raise_for_status behavior
from response.py. Extend test_sync_download_blob_read,
test_sync_download_blob_stream, and the count stream tests (or add focused tests
beside them) to simulate a non-2xx httpx.Response for client.download_blob,
resp.read(), resp.stream(), and client.count, then assert the expected error
hierarchy is raised for both sync and async paths.
packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py (1)

182-194: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Broaden query_params to match runtime values. _resolve_query_params() serializes nested dict/list values, but packages/nemo_platform_plugin/src/nemo_platform_plugin/client/types.py and packages/nemo_platform_plugin/src/nemo_platform_plugin/client/endpoint.py still type query_params as scalar-only. Update the shared alias to include the serialized shapes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py`
around lines 182 - 194, The shared query_params typing is too narrow for what
_resolve_query_params() actually accepts and serializes. Update the QueryParams
alias in types.py and the corresponding Endpoint.query_params annotation in
endpoint.py to allow nested dict/list values as well as the existing scalar
types, so the runtime serialization behavior in client.py is reflected
consistently across the client API.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py`:
- Around line 201-205: The ConflictError handling in client.send still falls
through into model validation when request.client_options includes exist_ok, so
suppressed 409 responses are not being skipped. Update the send() flow around
raise_for_status(raw) in client.py to return immediately for the suppressed
conflict case, or otherwise bypass model_validate(raw.json()) whenever a 409 is
intentionally accepted. Make sure both send() paths use the same early-exit
behavior tied to ConflictError and exist_ok.

In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/client/response.py`:
- Around line 112-116: The streaming parser in the `_iter()` helpers of
`response.py` treats every non-empty line as raw JSON, but SSE frames like
`data: {...}` will fail in `model_validate_json()`. Update the response
streaming logic to either parse SSE framing by stripping and decoding `data:`
payloads before validation, or narrow the `Response`/streaming classes so they
are explicitly NDJSON-only and do not claim SSE support. Apply the same fix to
both affected `_iter()` implementations.
- Around line 74-76: In the streaming response path of response.py,
`raise_for_status()` is called before the body is consumed, so `NemoHTTPError`
cannot read JSON `detail` from the response. Update the streaming methods in the
response handling code to read the body first (`raw.read()` for sync and `await
raw.aread()` for async) and only then call `raise_for_status()`, using the
existing stream context/response helpers to locate the affected logic.

---

Nitpick comments:
In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py`:
- Around line 182-194: The shared query_params typing is too narrow for what
_resolve_query_params() actually accepts and serializes. Update the QueryParams
alias in types.py and the corresponding Endpoint.query_params annotation in
endpoint.py to allow nested dict/list values as well as the existing scalar
types, so the runtime serialization behavior in client.py is reflected
consistently across the client API.

In `@plugins/example-plugin/tests/test_sdk.py`:
- Around line 247-301: Add the missing async parity test for blob streaming in
the existing test module by mirroring test_sync_download_blob_stream with a new
test_async_download_blob_stream. Use _async_client, set mock_http.stream with
_async_stream_ctx, call client.download_blob(name="pic.png"), and verify the
streamed chunks join to the expected bytes inside an async with resp.stream()
block.
- Around line 247-336: The streaming/binary response tests for download_blob and
count only cover success cases, so add error-path coverage to lock in the new
raise_for_status behavior from response.py. Extend test_sync_download_blob_read,
test_sync_download_blob_stream, and the count stream tests (or add focused tests
beside them) to simulate a non-2xx httpx.Response for client.download_blob,
resp.read(), resp.stream(), and client.count, then assert the expected error
hierarchy is raised for both sync and async paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 638bac52-2f0a-48f9-bfd4-19306172cd86

📥 Commits

Reviewing files that changed from the base of the PR and between 99604c5 and 307c2de.

📒 Files selected for processing (8)
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/adapter.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/endpoint.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/errors.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/response.py
  • packages/nemo_platform_plugin/tests/client/test_client.py
  • packages/nemo_platform_plugin/tests/client/test_client_options.py
  • plugins/example-plugin/tests/test_sdk.py

Comment thread packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py Outdated
Comment thread packages/nemo_platform_plugin/src/nemo_platform_plugin/client/response.py Outdated
Comment thread packages/nemo_platform_plugin/src/nemo_platform_plugin/client/response.py Outdated
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 22510/29625 76.0% 60.9%
Integration Tests 13002/28305 45.9% 19.4%

Comment thread packages/nemo_platform_plugin/src/nemo_platform_plugin/client/response.py Outdated
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
@matthewgrossman
matthewgrossman enabled auto-merge July 1, 2026 21:24
@matthewgrossman
matthewgrossman added this pull request to the merge queue Jul 1, 2026
Merged via the queue into main with commit 47d6eea Jul 1, 2026
56 checks passed
@matthewgrossman
matthewgrossman deleted the mgrossman/GENERIC1-aircore-827-migrate-first-plugin-to-nemoclient-typed-http-client-files- branch July 1, 2026 21:53
arpitsardhana pushed a commit that referenced this pull request Jul 9, 2026
* feat(client): Add nemoclient error hierarchy and fix streaming

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>

* remove exist_ok

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>

* fix sse and code review

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>

* code review

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>

---------

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants