Skip to content

feat(experiments): Add BE for new Experiment pinned_at field#349

Merged
shanaiabuggy merged 6 commits into
mainfrom
sbuggy/ase-298
Jun 22, 2026
Merged

feat(experiments): Add BE for new Experiment pinned_at field#349
shanaiabuggy merged 6 commits into
mainfrom
sbuggy/ase-298

Conversation

@shanaiabuggy

@shanaiabuggy shanaiabuggy commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added experiment pinning support, exposing a nullable pinned_at timestamp to distinguish pinned vs unpinned experiments.
    • Introduced dedicated pin/unpin endpoints to update pinned state.
    • Updated the experiments list endpoint to support filtering by pinned state (filter[is_pinned]=true/false) and sorting by pinned_at (including -pinned_at).
  • Documentation
    • Updated the API contract with the new pin/unpin endpoints, filter[is_pinned], and pinned_at sort options.
  • Tests
    • Added integration coverage for pin/unpin behavior, pinned-state filtering, and pinned_at sorting.

@shanaiabuggy
shanaiabuggy requested review from a team as code owners June 16, 2026 16:39
@github-actions github-actions Bot added the feat label Jun 16, 2026
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

Adds experiment pinning to the intake service: a new pinned_at field on Experiment, two endpoints (POST/DELETE on /experiments/{name}/pin), an is_pinned filter on list, authz rules mapping to intake.experiments.update, OpenAPI spec updates across three files, and integration tests covering the full lifecycle.

Changes

Experiment Pinning

Layer / File(s) Summary
Entity and API schema contracts
services/intake/src/nmp/intake/entities/experiments.py, services/intake/src/nmp/intake/api/v2/experiments/schemas.py
Experiment entity adds optional pinned_at: datetime | None. ExperimentResponse adds nullable pinned_at populated via from_entity. ExperimentFilter adds nullable is_pinned: bool | None.
Pin/unpin endpoints and list filter
services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
pin_experiment sets pinned_at to UTC now (idempotent); unpin_experiment clears it (idempotent). Both reject soft-deleted experiments and persist via entity_client.update. list_experiments extends sort field and applies _apply_is_pinned_filter to translate is_pinned to data.pinned_at null/non-null constraint.
Authorization rules
services/core/auth/src/nmp/core/auth/assets/static-authz.yaml
Maps POST /pin and DELETE /pin to intake.experiments.update permission, both requiring intake:write and platform:write scopes.
OpenAPI spec updates
openapi/openapi.yaml, openapi/ga/openapi.yaml, openapi/ga/individual/platform.openapi.yaml
All three specs add /pin path (POST/DELETE), extend ExperimentFilter with is_pinned, extend ExperimentResponse with nullable pinned_at, and update list filter and sort documentation.
Integration tests
services/intake/tests/integration/test_experiments_crud.py
Covers full pin lifecycle (set, refresh, unpin, idempotency), 404 for unknown/soft-deleted experiments, filter[is_pinned] list filtering (true/false/unfiltered), and sorting by pinned_at.

Possibly related PRs

  • NVIDIA-NeMo/nemo-platform#124: Introduces the core Experiments API schemas and CRUD endpoints; this PR extends that surface with pinned-state fields and pin/unpin operations.
  • NVIDIA-NeMo/nemo-platform#268: Also modifies experiments list filtering in endpoints.py by extending filter constraints; this PR stacks is_pinned filtering alongside existing soft-delete semantics.

Suggested reviewers

  • asutermo
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately describes the primary change: adding backend support for the pinned_at field in experiments.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sbuggy/ase-298

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

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

🤖 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 `@openapi/ga/individual/platform.openapi.yaml`:
- Around line 10501-10506: The `pinned_at` field description documents that it
can be null when unpinned, but the type definition only allows string values.
Update the type definition for `pinned_at` to allow both string and null values
using OpenAPI 3.1 syntax by changing `type: string` to `type: [string, null]` to
match the documented behavior and prevent client/validator errors when receiving
null responses.

In `@openapi/ga/openapi.yaml`:
- Around line 10501-10506: The pinned_at field schema in openapi/openapi.yaml,
openapi/ga/openapi.yaml, and openapi/ga/individual/platform.openapi.yaml
currently only allows type: string with format: date-time, but the description
states the field can be null when unpinned. Update the pinned_at field
definition in all three files to allow null values by adding nullable: true to
the schema, or by changing type from string to [string, null] to enable proper
deserialization when experiments are unpinned.

In `@openapi/openapi.yaml`:
- Around line 10501-10506: The pinned_at field schema currently only accepts
string values in date-time format, but the description states it should be null
when unpinned. Update the pinned_at field schema to allow null values alongside
the date-time string type by adding `nullable: true` to the schema definition,
ensuring generated clients will accept both null responses for unpinned
experiments and date-time strings for pinned experiments.
- Around line 3863-3866: The documentation in the OpenAPI specification
describes the pin operation as "Idempotent" but this is incorrect because the
operation updates the `pinned_at` timestamp to the current time on every call,
which changes state and ordering. Remove the claim that this operation is
idempotent, or replace it with accurate terminology such as "repeatable" that
correctly describes the behavior of re-pinning an already-pinned experiment
while acknowledging that the timestamp and ordering will change with each call.

In `@services/core/auth/src/nmp/core/auth/assets/static-authz.yaml`:
- Around line 949-958: The pin and unpin operations for experiments are
currently mapped to incorrect permissions because they mutate existing
experiment state (the `pinned_at` field) rather than creating or deleting
resources. Find the `pin` and `unpin` operation definitions in the
static-authz.yaml file and update them to use the `intake.experiments.update`
permission instead of their current permission mappings, while keeping
appropriate write scopes like `intake:write` and `platform:write`. This ensures
that only principals with update rights can perform these operations.
🪄 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: 1a6a506e-54b6-4082-9409-927664941a13

📥 Commits

Reviewing files that changed from the base of the PR and between 9af9189 and 47f3a88.

⛔ Files ignored due to path filters (19)
  • sdk/python/nemo-platform/.nmpcontext/openapi.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/.nmpcontext/stainless.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/experiments/api.md is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/experiments/experiments.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/files/api.md is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/files/filesets.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/__init__.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/experiments/experiment_filter_param.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/experiments/experiment_list_params.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/experiments/experiment_response.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/files/__init__.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/files/fileset.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_create_params.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/files/fileset_metadata_param.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/shared/__init__.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/shared/fileset_metadata.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/shared_params/fileset_metadata_param.py is excluded by !sdk/**
  • sdk/python/nemo-platform/tests/api_resources/test_experiments.py is excluded by !sdk/**
  • sdk/stainless.yaml is excluded by !sdk/**
📒 Files selected for processing (8)
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • services/core/auth/src/nmp/core/auth/assets/static-authz.yaml
  • services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
  • services/intake/src/nmp/intake/api/v2/experiments/schemas.py
  • services/intake/src/nmp/intake/entities/experiments.py
  • services/intake/tests/integration/test_experiments_crud.py

Comment thread openapi/ga/individual/platform.openapi.yaml
Comment thread openapi/ga/openapi.yaml
Comment thread openapi/openapi.yaml Outdated
Comment thread openapi/openapi.yaml
Comment thread services/core/auth/src/nmp/core/auth/assets/static-authz.yaml
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 20036/26510 75.6% 60.9%
Integration Tests 11693/25282 46.2% 19.8%

Signed-off-by: shanaiabuggy <[email protected]>

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

♻️ Duplicate comments (1)
openapi/ga/individual/platform.openapi.yaml (1)

10501-10507: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use OpenAPI 3.1 null syntax for pinned_at.

nullable: true is not the 3.1 schema form; clients/validators can still treat this as string-only. Model null explicitly.

Fix
         pinned_at:
           title: Pinned At
           description: Timestamp at which the experiment was pinned, or null if unpinned.
             Managed via POST/DELETE /experiments/{name}/pin.
-          nullable: true
-          type: string
-          format: date-time
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'

Verify with:

#!/bin/bash
set -euo pipefail

python - <<'PY'
from pathlib import Path

path = Path("openapi/ga/individual/platform.openapi.yaml")
text = path.read_text()

assert "openapi: 3.1.0" in "\n".join(text.splitlines()[:5])

start = text.index("        pinned_at:")
end = text.index("        evaluator_names:", start)
block = text[start:end]
print(block)

if "nullable: true" in block:
    raise SystemExit("pinned_at still uses nullable; use anyOf/type union with null for OpenAPI 3.1.")
if "type: 'null'" not in block and 'type: "null"' not in block:
    raise SystemExit("pinned_at does not explicitly allow null.")
PY
🤖 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 `@openapi/ga/individual/platform.openapi.yaml` around lines 10501 - 10507, The
pinned_at field in the platform.openapi.yaml file uses the OpenAPI 3.0 syntax
nullable: true, which is not valid for OpenAPI 3.1. Replace the nullable: true
line with an anyOf structure that explicitly defines the allowed types: string
with format date-time and null. This ensures the field properly declares that it
can be either a date-time string or null, compliant with OpenAPI 3.1 schema
requirements.
🤖 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 `@openapi/ga/individual/platform.openapi.yaml`:
- Around line 3863-3866: The documentation currently states that
most-recently-pinned experiments sort first when re-pinning occurs, but the GET
/experiments list endpoint does not reflect this behavior in its sorting
contract. Update the GET /experiments endpoint definition to either document the
default pin-aware ordering behavior (explaining that results are sorted by
pinned_at when experiments are pinned) or add pinned_at as an available sort
field in the sort parameter options alongside created_at, updated_at, and name.
Ensure the list endpoint contract aligns with the behavior described in the
re-pinning documentation.

---

Duplicate comments:
In `@openapi/ga/individual/platform.openapi.yaml`:
- Around line 10501-10507: The pinned_at field in the platform.openapi.yaml file
uses the OpenAPI 3.0 syntax nullable: true, which is not valid for OpenAPI 3.1.
Replace the nullable: true line with an anyOf structure that explicitly defines
the allowed types: string with format date-time and null. This ensures the field
properly declares that it can be either a date-time string or null, compliant
with OpenAPI 3.1 schema requirements.
🪄 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: 2e031e3d-8995-4e85-aff4-100297bbcae5

📥 Commits

Reviewing files that changed from the base of the PR and between 47f3a88 and 5873ea4.

⛔ Files ignored due to path filters (2)
  • sdk/python/nemo-platform/.nmpcontext/openapi.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/experiments/experiments.py is excluded by !sdk/**
📒 Files selected for processing (8)
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • services/core/auth/src/nmp/core/auth/assets/static-authz.yaml
  • services/core/models/tests/integration/test_model_entity_service_integration.py
  • services/core/models/tests/unit/test_model_entity_service_unit.py
  • services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
  • services/intake/src/nmp/intake/api/v2/experiments/schemas.py
✅ Files skipped from review due to trivial changes (1)
  • services/core/models/tests/integration/test_model_entity_service_integration.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • services/intake/src/nmp/intake/api/v2/experiments/schemas.py
  • services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
  • openapi/openapi.yaml

Comment thread openapi/ga/individual/platform.openapi.yaml
Signed-off-by: shanaiabuggy <[email protected]>

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

🧹 Nitpick comments (1)
services/intake/tests/integration/test_experiments_crud.py (1)

417-422: ⚡ Quick win

Also test unpin (DELETE) for consistency.

Line 421 only tests POST /pin. Test DELETE /pin as well to match the pattern in test_pin_unknown_experiment_returns_404 (lines 413-414).

Suggested addition
     assert client.delete(f"{EXPERIMENTS}/exp-soft").status_code == 204
     assert client.post(f"{EXPERIMENTS}/exp-soft/pin").status_code == 404
+    assert client.delete(f"{EXPERIMENTS}/exp-soft/pin").status_code == 404
🤖 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 `@services/intake/tests/integration/test_experiments_crud.py` around lines 417
- 422, The test_pin_rejects_deleted_experiment function currently only tests the
POST endpoint for pinning a deleted experiment but should also test the DELETE
endpoint for unpinning to be consistent with the pattern used in
test_pin_unknown_experiment_returns_404. Add an assertion that tests the DELETE
request to the pin endpoint on the deleted experiment, verifying that it also
returns a 404 status code, matching the structure of the existing POST
assertion.
🤖 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.

Nitpick comments:
In `@services/intake/tests/integration/test_experiments_crud.py`:
- Around line 417-422: The test_pin_rejects_deleted_experiment function
currently only tests the POST endpoint for pinning a deleted experiment but
should also test the DELETE endpoint for unpinning to be consistent with the
pattern used in test_pin_unknown_experiment_returns_404. Add an assertion that
tests the DELETE request to the pin endpoint on the deleted experiment,
verifying that it also returns a 404 status code, matching the structure of the
existing POST assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 386a0c0e-b282-440e-868d-18c574db7f0a

📥 Commits

Reviewing files that changed from the base of the PR and between 5873ea4 and 9daeeee.

⛔ Files ignored due to path filters (3)
  • sdk/python/nemo-platform/.nmpcontext/openapi.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/experiments/experiments.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/experiments/experiment_list_params.py is excluded by !sdk/**
📒 Files selected for processing (5)
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
  • services/intake/tests/integration/test_experiments_crud.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • openapi/ga/openapi.yaml
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/openapi.yaml
  • services/intake/src/nmp/intake/api/v2/experiments/endpoints.py

Comment thread services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
Signed-off-by: shanaiabuggy <[email protected]>
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