Skip to content

feat(client): Create a new entities client#583

Open
matthewgrossman wants to merge 3 commits into
mainfrom
mgrossman/aircore-875-migrate-entities-service-to-nemoclient-typed-http-client
Open

feat(client): Create a new entities client#583
matthewgrossman wants to merge 3 commits into
mainfrom
mgrossman/aircore-875-migrate-entities-service-to-nemoclient-typed-http-client

Conversation

@matthewgrossman

@matthewgrossman matthewgrossman commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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

Summary by CodeRabbit

  • New Features

    • Added a new entity store resource for easier create, read, update, delete, and list workflows.
    • Added support for accessing entity data by name, ID, or field values, with async and sync client options.
  • Bug Fixes

    • Retry handling now works by default, so transient request failures are retried automatically.
    • Pagination is more reliable, stopping on empty pages and avoiding repeated parsing of the same page.
  • Chores

    • Expanded public exports and default service wiring so the new entity store features are available in more entry points.

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

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR makes retry enabled by default in NemoClient/AsyncNemoClient, fixes pagination to stop on empty pages and advance using server-echoed page numbers with first-page caching, and introduces a new EntityStoreResource with typed Entity Store endpoints, client, DI wiring, and error mapping across nemo_platform_plugin and nmp_common.

Changes

Retry, pagination, and entity resource changes

Layer / File(s) Summary
Default retry policy
.../client/types.py, .../client/client.py, tests/client/test_client_options.py
DEFAULT_RETRY_POLICY constant added and applied as the default retry argument for BaseNemoClient, NemoClient, AsyncNemoClient; retry property return type changed to non-optional; tests cover default-retry and opt-out behavior.
Pagination hardening
.../client/response.py, tests/client/test_pagination.py
Sync/async _next_page helpers stop iteration on empty pages and advance using server-echoed page metadata instead of a local counter; first parsed page is cached.
Entity Store types, endpoints, and low-level client
.../entities/types.py, .../entities/endpoints.py, .../entities/client.py, tests/entities/test_endpoints.py
New Pydantic models (EntityCreateInput, EntityUpdate, EntityResponse, DeleteResponse) and query-param types; abstract endpoint declarations for create/list/get/update/delete; EntitiesClient/AsyncEntitiesClient bind endpoints to sync/async base clients.
EntityStoreResource implementation
.../entities/resource.py, .../entities/__init__.py, tests/entities/test_resource.py
High-level async EntityStoreResource with CRUD, list, save, add, get_by_field, as_service impersonation, and mapping of transport errors to EntityConflictError/EntityNotFoundError/EntityValidationError; re-exported from entities package.
DI wiring and re-exports
.../dependencies.py, nmp_common/.../base.py, nmp_common/.../dependencies.py, nmp_common/.../entities/*, nmp_common/.../service/__init__.py
Adds get_entity_store_resource() dependency and DependencyProvider.get_entity_store_resource(); registers dependency override; re-exports EntityStoreResource/get_entity_store_resource through nmp_common.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant EntityStoreResource
  participant AsyncEntitiesClient
  participant EntityStoreServer

  Caller->>EntityStoreResource: create(entity)
  EntityStoreResource->>AsyncEntitiesClient: create_entity(body)
  AsyncEntitiesClient->>EntityStoreServer: POST /entities
  EntityStoreServer-->>AsyncEntitiesClient: EntityResponse or 409
  AsyncEntitiesClient-->>EntityStoreResource: response or ConflictError
  EntityStoreResource-->>Caller: EntityT or EntityConflictError
Loading
sequenceDiagram
  participant NemoClient
  participant HTTPServer
  participant PaginatedResponse

  NemoClient->>HTTPServer: request (DEFAULT_RETRY_POLICY)
  HTTPServer-->>NemoClient: 503, retry, then success
  NemoClient->>PaginatedResponse: parse page, cache first page
  PaginatedResponse->>HTTPServer: fetch next (server-echoed page)
  HTTPServer-->>PaginatedResponse: empty items, stop
Loading

Possibly related PRs

Suggested reviewers: maxdubrinsky, mckornfield

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and correctly points to the new entities client, which is a real and central part of the change.
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.
✨ 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/aircore-875-migrate-entities-service-to-nemoclient-typed-http-client

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

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 23527/30733 76.5% 61.3%
Integration Tests 13758/29413 46.8% 19.9%

@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: 6

🧹 Nitpick comments (1)
packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/entity_client.py (1)

138-173: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache TypeAdapter instances instead of rebuilding per entity.

TypeAdapter(entity_type) and TypeAdapter(attr_type) are constructed fresh on every call — this runs once per item during pagination (_MappedPaginatedResponse._convert/items()), so listing many entities rebuilds adapters repeatedly. Cache by type (e.g. functools.lru_cache on a small helper, or a module-level dict) to avoid repeated schema-build cost.

♻️ Suggested caching approach
+from functools import lru_cache
+
+@lru_cache(maxsize=None)
+def _adapter_for(tp: Any) -> TypeAdapter:
+    return TypeAdapter(tp)
+
 def _convert_api_entity_to_model(self, entity: EntityResponse, entity_type: EntityTypeLike) -> EntityT:
     entity_dict = entity.model_dump()
     entity_dict.update(entity.data)
-    type_adapter = TypeAdapter(entity_type)
+    type_adapter = _adapter_for(entity_type)
     result = type_adapter.validate_python(entity_dict)
     ...
-                setattr(result, field_name, TypeAdapter(attr_type).validate_python(raw_value))
+                setattr(result, field_name, _adapter_for(attr_type).validate_python(raw_value))
🤖 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/entities/entity_client.py`
around lines 138 - 173, The entity conversion path rebuilds TypeAdapter objects
on every item in _convert_api_entity_to_model, which is expensive during
paginated listing. Add a cached helper in entity_client.py for both entity_type
and attr_type adapter creation (for example via a small lru_cache-backed
function or module-level cache), then use that helper instead of calling
TypeAdapter(...) directly in _convert_api_entity_to_model. Keep the existing
validation logic intact while reusing adapters across repeated calls.
🤖 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/types.py`:
- Around line 207-211: The default retry policy in the
RetryPolicy/DEFAULT_RETRY_POLICY path is currently applied too broadly and can
replay write request bodies without protection. Update the retry logic so it
only retries safe/idempotent methods by default, and only allows POST/PATCH/PUT
retries when an Idempotency-Key guard is present or equivalent idempotency check
exists. Make the change in the retry-policy setup and any request-classification
helper used by the Nemo platform client types so transient errors like
502/503/504/429 are not blindly retried for non-idempotent writes.

In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/dependencies.py`:
- Line 17: Remove the TYPE_CHECKING-only handling for NemoEntityClient in
dependencies.py and import it unconditionally from nemo_platform_plugin.entities
alongside EntityClient. Update the dependency provider function that currently
uses the runtime import in the body so its return annotation uses the concrete
NemoEntityClient type instead of a string literal, and keep the symbol names
EntityClient, NemoEntityClient, and the provider function in sync with the
same-package runtime import rule.
- Around line 65-85: `get_nemo_entity_client()` currently creates a fresh async
transport via `get_async_nemo_client()` and wraps it in `NemoEntityClient`
without any cleanup, which can leak sockets over repeated calls. Update the
dependency to manage the client lifecycle explicitly: either reuse a shared
async client instance or make this an async dependency that closes the
`AsyncNemoClient`/`httpx.AsyncClient` after use. Keep the fix localized to
`get_nemo_entity_client`, `get_async_nemo_client`, and the `NemoEntityClient`
construction path so the caller does not retain an unclosed client.

In `@packages/nemo_platform_plugin/tests/client/test_client_options.py`:
- Around line 217-237: The test currently bypasses the constructor default by
passing an explicit RetryPolicy in test_default_policy_retries, so it won’t
catch regressions in NemoClient’s default retry behavior. Update this test to
construct NemoClient without retry= and instead patch the retry backoff/sleep
path to keep it fast, while still asserting the 503 is retried 1 + 3 times via
mock_http.request and the raised NemoHTTPError status_code remains 503.

In `@packages/nemo_platform_plugin/tests/client/test_pagination.py`:
- Around line 430-445: The test is overriding the client’s default retry policy
after construction, so it no longer verifies the constructor-installed behavior.
In test_retries_on_by_default, keep NemoClient untouched after instantiation and
instead patch the retry backoff/sleep behavior used by the retry path so the
default DEFAULT_RETRY_POLICY is what gets exercised. Use the existing
NemoClient, RetryPolicy, and send/LIST_ITEMS setup to confirm 503s are retried
without mutating client._retry.

In `@packages/nmp_common/src/nmp/common/service/base.py`:
- Around line 37-39: Remove the TYPE_CHECKING-only import for NemoEntityClient
in base.py and import it normally at module scope, since the evidence in
client.py shows the plain import is safe. Then update the affected return type
annotations in the Base service methods to use the concrete NemoEntityClient
type instead of quoted forward references, keeping the existing method
signatures and names intact.

---

Nitpick comments:
In
`@packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/entity_client.py`:
- Around line 138-173: The entity conversion path rebuilds TypeAdapter objects
on every item in _convert_api_entity_to_model, which is expensive during
paginated listing. Add a cached helper in entity_client.py for both entity_type
and attr_type adapter creation (for example via a small lru_cache-backed
function or module-level cache), then use that helper instead of calling
TypeAdapter(...) directly in _convert_api_entity_to_model. Keep the existing
validation logic intact while reusing adapters across repeated calls.
🪄 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: 3087fe54-f7fb-46d4-b0a6-451b28f497f2

📥 Commits

Reviewing files that changed from the base of the PR and between 954c403 and 5a63353.

📒 Files selected for processing (19)
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/response.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/client/types.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/dependencies.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/__init__.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/client.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/endpoints.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/entity_client.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/legacy.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/types.py
  • packages/nemo_platform_plugin/tests/client/test_client_options.py
  • packages/nemo_platform_plugin/tests/client/test_pagination.py
  • packages/nemo_platform_plugin/tests/entities/test_endpoints.py
  • packages/nemo_platform_plugin/tests/entities/test_nemo_entity_client.py
  • packages/nmp_common/src/nmp/common/entities/__init__.py
  • packages/nmp_common/src/nmp/common/entities/client.py
  • packages/nmp_common/src/nmp/common/service/__init__.py
  • packages/nmp_common/src/nmp/common/service/base.py
  • packages/nmp_common/src/nmp/common/service/dependencies.py

Comment on lines +207 to +211
# Shared default: retries transient failures out of the box. The retryable set
# is deliberately conservative (pre-processing failures only), so replaying a
# non-safe method is safe — notably 409 is excluded because it carries the
# entity store's optimistic-lock conflict, which must never be retried blindly.
DEFAULT_RETRY_POLICY = RetryPolicy()

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Don’t default-retry write requests without an idempotency guard.

This policy is now the default, and the retry loop replays request bodies. 502/503/504/429 can occur after backend processing, so POST/PATCH/PUT may be duplicated or surface false conflicts. Gate default retries to safe/idempotent methods, or require an Idempotency-Key before retrying writes.

🤖 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/types.py`
around lines 207 - 211, The default retry policy in the
RetryPolicy/DEFAULT_RETRY_POLICY path is currently applied too broadly and can
replay write request bodies without protection. Update the retry logic so it
only retries safe/idempotent methods by default, and only allows POST/PATCH/PUT
retries when an Idempotency-Key guard is present or equivalent idempotency check
exists. Make the change in the retry-policy setup and any request-classification
helper used by the Nemo platform client types so transient errors like
502/503/504/429 are not blindly retried for non-idempotent writes.

from nemo_platform import AsyncNeMoPlatform
from nemo_platform_plugin.config import PlatformConfig
from nemo_platform_plugin.entities import EntityClient
from nemo_platform_plugin.entities import EntityClient, NemoEntityClient

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Drop TYPE_CHECKING guard for NemoEntityClient — same-package import.

NemoEntityClient lives in nemo_platform_plugin.entities, the same package as this file, and is already imported normally at runtime inside the function body (Line 83). Per path guidelines this class should be imported unconditionally instead of hidden behind TYPE_CHECKING, with a concrete (non-string) return type.

🔧 Proposed fix
-if TYPE_CHECKING:
-    from nemo_platform_plugin.entities import EntityClient, NemoEntityClient
+from nemo_platform_plugin.entities import NemoEntityClient
+
+if TYPE_CHECKING:
+    from nemo_platform_plugin.entities import EntityClient
-def get_nemo_entity_client() -> "NemoEntityClient":
+def get_nemo_entity_client() -> NemoEntityClient:
     ...
-    from nemo_platform_plugin.client_provider import get_async_nemo_client
-    from nemo_platform_plugin.entities import NemoEntityClient
-
+    from nemo_platform_plugin.client_provider import get_async_nemo_client
+
     return NemoEntityClient(get_async_nemo_client())

As per path instructions for packages/nemo_platform_plugin/src/nemo_*/**/*.py: "do not use TYPE_CHECKING imports for types that are available at runtime in the same package; import them normally instead."

Also applies to: 65-85

🤖 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/dependencies.py` at
line 17, Remove the TYPE_CHECKING-only handling for NemoEntityClient in
dependencies.py and import it unconditionally from nemo_platform_plugin.entities
alongside EntityClient. Update the dependency provider function that currently
uses the runtime import in the body so its return annotation uses the concrete
NemoEntityClient type instead of a string literal, and keep the symbol names
EntityClient, NemoEntityClient, and the provider function in sync with the
same-package runtime import rule.

Source: Path instructions

Comment on lines +65 to +85


def get_nemo_entity_client() -> "NemoEntityClient":
"""Return a NemoClient-backed :class:`NemoEntityClient`.

Unlike the sibling placeholders (``get_sdk_client``, ``get_entity_client``,
``get_platform_config``), this has a real default implementation because
everything it needs lives in ``nemo_platform_plugin``: the client transport,
the env-based context/header builder (``client_provider``), and the entity
client itself — no ``nmp-core`` bits required.

The default builds an env-scoped client (``NMP_BASE_URL`` / ``NMP_PRINCIPAL``
via ``client_provider.get_async_nemo_client``), which is the correct context
for task containers, background jobs, and controllers. For request handling,
``Service`` still overrides this via ``app.dependency_overrides`` with a
request-scoped client (service-principal + on-behalf-of headers).
"""
from nemo_platform_plugin.client_provider import get_async_nemo_client
from nemo_platform_plugin.entities import NemoEntityClient

return NemoEntityClient(get_async_nemo_client())

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd client_provider.py --exec cat -n {}

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 4649


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## files\n'
git ls-files | rg '^(packages/nemo_platform_plugin/src/nemo_platform_plugin/(client|entities|dependencies)\.py|.*client_provider\.py|.*entities\.py)$'

for f in \
  packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py \
  packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py \
  packages/nemo_platform_plugin/src/nemo_platform_plugin/dependencies.py \
  packages/nemo_platform_plugin/src/nemo_platform_plugin/client_provider.py
do
  if [ -f "$f" ]; then
    printf '\n## outline: %s\n' "$f"
    ast-grep outline "$f" --view expanded || true
    printf '\n## head: %s\n' "$f"
    wc -l "$f"
  fi
done

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 4762


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

paths = [
    Path("packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py"),
    Path("packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py"),
    Path("packages/nemo_platform_plugin/src/nemo_platform_plugin/client_provider.py"),
    Path("packages/nemo_platform_plugin/src/nemo_platform_plugin/dependencies.py"),
]
for p in paths:
    if p.exists():
        print(f"\n### {p} ({sum(1 for _ in p.open())} lines)")
        for i, line in enumerate(p.open(), 1):
            if any(k in line for k in ["class AsyncNemoClient", "class NemoClient", "def close", "def __aenter__", "def __aexit__", "def __init__", "class NemoEntityClient", "def get_async_nemo_client", "def get_nemo_entity_client", "async def", "httpx", "AsyncClient", "Client"]):
                print(f"{i:4d}: {line.rstrip()}")
PY

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 3559


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate implementations and lifecycle hooks
rg -n --hidden --glob '!**/.git/**' \
  -e 'class AsyncNemoClient|class NemoClient|def close|async def close|__aenter__|__aexit__|httpx\.AsyncClient|httpx\.Client|NemoEntityClient|get_async_nemo_client|get_nemo_entity_client' \
  packages/nemo_platform_plugin/src/nemo_platform_plugin

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 4751


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/entity_client.py
wc -l "$file"
sed -n '1,240p' "$file"

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 10511


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## dependencies\n'
sed -n '1,140p' packages/nemo_platform_plugin/src/nemo_platform_plugin/dependencies.py

printf '\n## client ctor/close\n'
sed -n '433,490p' packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 5459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## entity client\n'
sed -n '1,240p' packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/entity_client.py

printf '\n## dependencies\n'
sed -n '1,140p' packages/nemo_platform_plugin/src/nemo_platform_plugin/dependencies.py

printf '\n## async client lifecycle\n'
sed -n '433,490p' packages/nemo_platform_plugin/src/nemo_platform_plugin/client/client.py

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 15745


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## call sites\n'
rg -n --hidden --glob '!**/.git/**' \
  -e 'get_nemo_entity_client\(' \
  -e 'NemoEntityClient\(' \
  -e '\.close\(\)' \
  packages/nemo_platform_plugin/src plugins services sdk e2e

printf '\n## dependency overrides / app wiring\n'
rg -n --hidden --glob '!**/.git/**' \
  -e 'dependency_overrides' \
  -e 'get_nemo_entity_client' \
  -e 'yield .*NemoEntityClient|async def .*close' \
  packages/nemo_platform_plugin/src plugins services sdk

printf '\n## nearby service lifecycle code\n'
fd -a 'service*.py' packages/nemo_platform_plugin/src services plugins sdk | head -n 50

Repository: NVIDIA-NeMo/nemo-platform

Length of output: 48877


Scope or close the async entity client get_nemo_entity_client() creates a new AsyncNemoClient/httpx.AsyncClient each call, but nothing here closes it. If this dependency is used repeatedly in jobs/controllers, add lifecycle management or reuse a shared client to avoid leaking sockets.

🤖 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/dependencies.py`
around lines 65 - 85, `get_nemo_entity_client()` currently creates a fresh async
transport via `get_async_nemo_client()` and wraps it in `NemoEntityClient`
without any cleanup, which can leak sockets over repeated calls. Update the
dependency to manage the client lifecycle explicitly: either reuse a shared
async client instance or make this an async dependency that closes the
`AsyncNemoClient`/`httpx.AsyncClient` after use. Keep the fix localized to
`get_nemo_entity_client`, `get_async_nemo_client`, and the `NemoEntityClient`
construction path so the caller does not retain an unclosed client.

Comment on lines +217 to +237
def test_default_policy_retries(self) -> None:
"""Without an explicit policy, the client retries transient failures.

DEFAULT_RETRY_POLICY is applied by the constructor (max_retries=3), so a
persistent 503 is attempted 1 + 3 times before surfacing.
"""
mock_http = MagicMock(spec=httpx.Client)
mock_http.request.return_value = httpx.Response(
503,
request=httpx.Request("GET", f"{BASE}/apis/test/v2/items/alice"),
json={"detail": "unavailable"},
)

# Use the default policy's codes but zero backoff to keep the test fast.
client = NemoClient(base_url=BASE, http_client=mock_http, retry=RetryPolicy(backoff_base=0.0))

with pytest.raises(NemoHTTPError) as exc_info:
client.send(GET_ITEM(name="alice"))

assert exc_info.value.status_code == 503
assert mock_http.request.call_count == 4 # 1 initial + 3 retries

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This doesn’t test the default policy.

Line 231 passes an explicit RetryPolicy, so the test would pass even if the constructor default regressed to None. Patch sleep instead and omit retry=.

Proposed fix
-    def test_default_policy_retries(self) -> None:
+    def test_default_policy_retries(self, monkeypatch: pytest.MonkeyPatch) -> None:
@@
-        # Use the default policy's codes but zero backoff to keep the test fast.
-        client = NemoClient(base_url=BASE, http_client=mock_http, retry=RetryPolicy(backoff_base=0.0))
+        monkeypatch.setattr("nemo_platform_plugin.client.client.time.sleep", lambda _: None)
+        client = NemoClient(base_url=BASE, http_client=mock_http)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_default_policy_retries(self) -> None:
"""Without an explicit policy, the client retries transient failures.
DEFAULT_RETRY_POLICY is applied by the constructor (max_retries=3), so a
persistent 503 is attempted 1 + 3 times before surfacing.
"""
mock_http = MagicMock(spec=httpx.Client)
mock_http.request.return_value = httpx.Response(
503,
request=httpx.Request("GET", f"{BASE}/apis/test/v2/items/alice"),
json={"detail": "unavailable"},
)
# Use the default policy's codes but zero backoff to keep the test fast.
client = NemoClient(base_url=BASE, http_client=mock_http, retry=RetryPolicy(backoff_base=0.0))
with pytest.raises(NemoHTTPError) as exc_info:
client.send(GET_ITEM(name="alice"))
assert exc_info.value.status_code == 503
assert mock_http.request.call_count == 4 # 1 initial + 3 retries
def test_default_policy_retries(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Without an explicit policy, the client retries transient failures.
DEFAULT_RETRY_POLICY is applied by the constructor (max_retries=3), so a
persistent 503 is attempted 1 + 3 times before surfacing.
"""
mock_http = MagicMock(spec=httpx.Client)
mock_http.request.return_value = httpx.Response(
503,
request=httpx.Request("GET", f"{BASE}/apis/test/v2/items/alice"),
json={"detail": "unavailable"},
)
monkeypatch.setattr("nemo_platform_plugin.client.client.time.sleep", lambda _: None)
client = NemoClient(base_url=BASE, http_client=mock_http)
with pytest.raises(NemoHTTPError) as exc_info:
client.send(GET_ITEM(name="alice"))
assert exc_info.value.status_code == 503
assert mock_http.request.call_count == 4 # 1 initial + 3 retries
🤖 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/tests/client/test_client_options.py` around
lines 217 - 237, The test currently bypasses the constructor default by passing
an explicit RetryPolicy in test_default_policy_retries, so it won’t catch
regressions in NemoClient’s default retry behavior. Update this test to
construct NemoClient without retry= and instead patch the retry backoff/sleep
path to keep it fast, while still asserting the 503 is retried 1 + 3 times via
mock_http.request and the raised NemoHTTPError status_code remains 503.

Comment on lines +430 to +445
def test_retries_on_by_default(self) -> None:
"""A client constructed without an explicit retry policy still retries 503s."""
mock_http = MagicMock(spec=httpx.Client)
mock_http.request.side_effect = [
httpx.Response(
503,
request=httpx.Request("GET", f"{BASE}/apis/test/v2/workspaces/default/items"),
json={"detail": "unavailable"},
),
_page_response([{"id": 1, "name": "a"}], page=1, total_pages=1),
]

# No retry= passed → DEFAULT_RETRY_POLICY (backoff patched to 0 for speed).
client = NemoClient(base_url=BASE, workspace="default", http_client=mock_http)
client._retry = RetryPolicy(backoff_base=0.0) # keep default codes, zero sleep
items = list(client.send(LIST_ITEMS()).items())

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid overriding the default under test.

Line 444 mutates _retry, so this no longer proves the constructor-installed default works. Patch sleep and leave the client untouched.

Proposed fix
-    def test_retries_on_by_default(self) -> None:
+    def test_retries_on_by_default(self, monkeypatch: pytest.MonkeyPatch) -> None:
@@
         # No retry= passed → DEFAULT_RETRY_POLICY (backoff patched to 0 for speed).
+        monkeypatch.setattr("nemo_platform_plugin.client.client.time.sleep", lambda _: None)
         client = NemoClient(base_url=BASE, workspace="default", http_client=mock_http)
-        client._retry = RetryPolicy(backoff_base=0.0)  # keep default codes, zero sleep
         items = list(client.send(LIST_ITEMS()).items())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_retries_on_by_default(self) -> None:
"""A client constructed without an explicit retry policy still retries 503s."""
mock_http = MagicMock(spec=httpx.Client)
mock_http.request.side_effect = [
httpx.Response(
503,
request=httpx.Request("GET", f"{BASE}/apis/test/v2/workspaces/default/items"),
json={"detail": "unavailable"},
),
_page_response([{"id": 1, "name": "a"}], page=1, total_pages=1),
]
# No retry= passed → DEFAULT_RETRY_POLICY (backoff patched to 0 for speed).
client = NemoClient(base_url=BASE, workspace="default", http_client=mock_http)
client._retry = RetryPolicy(backoff_base=0.0) # keep default codes, zero sleep
items = list(client.send(LIST_ITEMS()).items())
def test_retries_on_by_default(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""A client constructed without an explicit retry policy still retries 503s."""
mock_http = MagicMock(spec=httpx.Client)
mock_http.request.side_effect = [
httpx.Response(
503,
request=httpx.Request("GET", f"{BASE}/apis/test/v2/workspaces/default/items"),
json={"detail": "unavailable"},
),
_page_response([{"id": 1, "name": "a"}], page=1, total_pages=1),
]
# No retry= passed → DEFAULT_RETRY_POLICY (backoff patched to 0 for speed).
monkeypatch.setattr("nemo_platform_plugin.client.client.time.sleep", lambda _: None)
client = NemoClient(base_url=BASE, workspace="default", http_client=mock_http)
items = list(client.send(LIST_ITEMS()).items())
🤖 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/tests/client/test_pagination.py` around lines
430 - 445, The test is overriding the client’s default retry policy after
construction, so it no longer verifies the constructor-installed behavior. In
test_retries_on_by_default, keep NemoClient untouched after instantiation and
instead patch the retry backoff/sleep behavior used by the retry path so the
default DEFAULT_RETRY_POLICY is what gets exercised. Use the existing
NemoClient, RetryPolicy, and send/LIST_ITEMS setup to confirm 503s are retried
without mutating client._retry.

Comment on lines +37 to +39
if TYPE_CHECKING:
from nemo_platform_plugin.entities import NemoEntityClient

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Drop TYPE_CHECKING guard for NemoEntityClient — cross-file evidence shows plain import is safe.

packages/nmp_common/src/nmp/common/entities/client.py already does from nemo_platform_plugin.entities import NemoEntityClient as NemoEntityClient as a plain top-level import with no circular-import issue, so gating this import behind TYPE_CHECKING here isn't necessary. Import it normally and use a concrete return type instead of the quoted forward reference.

🔧 Proposed fix
-if TYPE_CHECKING:
-    from nemo_platform_plugin.entities import NemoEntityClient
+from nemo_platform_plugin.entities import NemoEntityClient
-    def get_nemo_entity_client(self, as_service: str | None = None) -> "NemoEntityClient":
+    def get_nemo_entity_client(self, as_service: str | None = None) -> NemoEntityClient:

As per coding guidelines: "prefer concrete type hints over string-based type hints, and do not import those types only under TYPE_CHECKING; import them normally when possible."

Also applies to: 158-158

🤖 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/nmp_common/src/nmp/common/service/base.py` around lines 37 - 39,
Remove the TYPE_CHECKING-only import for NemoEntityClient in base.py and import
it normally at module scope, since the evidence in client.py shows the plain
import is safe. Then update the affected return type annotations in the Base
service methods to use the concrete NemoEntityClient type instead of quoted
forward references, keeping the existing method signatures and names intact.

Source: Coding guidelines

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/resource.py (1)

365-381: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

delete_by_id never validates/uses the entity_type parameter.

The method resolves workspace/entity_type/name from the fetched EntityResponse (entity.entity_type, entity.workspace, entity.name) but the caller-supplied entity_type is unused. Compare with delete(), which correctly validates via _get_entity_type(entity_type). As written, delete_by_id(Widget, some_id) will happily delete an entity of a completely different type if some_id doesn't belong to a Widget.

🛡️ Proposed fix: validate fetched entity type matches caller expectation
     async def delete_by_id(self, entity_type: EntityTypeLike, entity_id: str) -> DeleteResponse:
         """Delete an entity by UUID (fetches it first to resolve name/workspace)."""
         try:
             entity = (await self._client.get_entity_by_id(id=entity_id)).data()
+            if entity.entity_type != _get_entity_type(entity_type):
+                raise EntityNotFoundError(f"Entity with id '{entity_id}' not found")
             query: ParentQueryParams = {}
             if entity.parent is not None:
                 query["parent"] = entity.parent
🤖 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/entities/resource.py`
around lines 365 - 381, `delete_by_id` is ignoring the caller-provided
`entity_type`, so it can delete an entity of a different kind than expected.
Update `delete_by_id` in `Resource` to validate the requested type with
`_get_entity_type(entity_type)` and compare it against the fetched
`entity.entity_type` before calling `_client.delete_entity_by_name`; if they
differ, raise an appropriate error instead of proceeding. Keep the existing
`get_entity_by_id` / `delete_entity_by_name` flow and only use the resolved
entity data after the type check passes.
♻️ Duplicate comments (1)
packages/nemo_platform_plugin/src/nemo_platform_plugin/dependencies.py (1)

17-17: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Drop TYPE_CHECKING guard for EntityStoreResource — same issue as prior NemoEntityClient flag.

EntityStoreResource lives in nemo_platform_plugin.entities (same package) and is already imported normally at runtime inside get_entity_store_resource (line 83). Gating it behind TYPE_CHECKING and using the quoted return type at line 67 repeats the exact pattern previously flagged for NemoEntityClient in this file.

🔧 Proposed fix
-if TYPE_CHECKING:
-    from nemo_platform_plugin.entities import EntityClient, EntityStoreResource
+from nemo_platform_plugin.entities import EntityStoreResource
+
+if TYPE_CHECKING:
+    from nemo_platform_plugin.entities import EntityClient
-def get_entity_store_resource() -> "EntityStoreResource":
+def get_entity_store_resource() -> EntityStoreResource:
     ...
     from nemo_platform_plugin.client_provider import get_async_nemo_client
-    from nemo_platform_plugin.entities import EntityStoreResource
-
     return EntityStoreResource.from_client(get_async_nemo_client())

As per path instructions for packages/nemo_platform_plugin/src/nemo_*/**/*.py: "do not use TYPE_CHECKING imports for types that are available at runtime in the same package; import them normally instead."

Also applies to: 67-67, 82-85

🤖 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/dependencies.py` at
line 17, The `EntityStoreResource` import is incorrectly guarded by
`TYPE_CHECKING`, even though it is a same-package runtime symbol already used by
`get_entity_store_resource`. Move `EntityStoreResource` to the normal import
list in `dependencies.py`, and update the `get_entity_store_resource` return
annotation to use the direct type name instead of a quoted string, matching the
existing runtime import pattern used for `EntityClient`.

Source: Path instructions

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

Outside diff comments:
In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/resource.py`:
- Around line 365-381: `delete_by_id` is ignoring the caller-provided
`entity_type`, so it can delete an entity of a different kind than expected.
Update `delete_by_id` in `Resource` to validate the requested type with
`_get_entity_type(entity_type)` and compare it against the fetched
`entity.entity_type` before calling `_client.delete_entity_by_name`; if they
differ, raise an appropriate error instead of proceeding. Keep the existing
`get_entity_by_id` / `delete_entity_by_name` flow and only use the resolved
entity data after the type check passes.

---

Duplicate comments:
In `@packages/nemo_platform_plugin/src/nemo_platform_plugin/dependencies.py`:
- Line 17: The `EntityStoreResource` import is incorrectly guarded by
`TYPE_CHECKING`, even though it is a same-package runtime symbol already used by
`get_entity_store_resource`. Move `EntityStoreResource` to the normal import
list in `dependencies.py`, and update the `get_entity_store_resource` return
annotation to use the direct type name instead of a quoted string, matching the
existing runtime import pattern used for `EntityClient`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 819b8e00-3ddf-448a-825b-0a520e1d047c

📥 Commits

Reviewing files that changed from the base of the PR and between 5a63353 and 868f732.

📒 Files selected for processing (10)
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/dependencies.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/__init__.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/client.py
  • packages/nemo_platform_plugin/src/nemo_platform_plugin/entities/resource.py
  • packages/nemo_platform_plugin/tests/entities/test_resource.py
  • packages/nmp_common/src/nmp/common/entities/__init__.py
  • packages/nmp_common/src/nmp/common/entities/client.py
  • packages/nmp_common/src/nmp/common/service/__init__.py
  • packages/nmp_common/src/nmp/common/service/base.py
  • packages/nmp_common/src/nmp/common/service/dependencies.py
✅ Files skipped from review due to trivial changes (1)
  • packages/nmp_common/src/nmp/common/service/dependencies.py

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.

1 participant