From afbfacb1d1c96c32356af60b401930566ede83e2 Mon Sep 17 00:00:00 2001 From: Albert Cui Date: Fri, 17 Jul 2026 22:47:18 -0400 Subject: [PATCH 1/9] fix(openapi): give colliding platform schemas distinct names #737 fixed the Automodel/Unsloth backends, but two collisions remained across the merged platform services and were silently collapsed by the schema-name normalizer: - DeleteResponse: core-entities' model (required id + deleted_count) collided with the shared nmp.common.api.common.DeleteResponse (id, deleted_at); guardrails had redefined the shared shape locally. - GenericSortField: core-entities, guardrails, and common each defined a different enum under the same name. The merge kept only the first-seen schema and repointed every $ref to it, so delete/sort endpoints across services referenced the wrong contract in the generated SDK. Give each its true name (schema-name change only; JSON wire format unchanged): - guardrails: drop the local DeleteResponse, import the byte-identical shared nmp.common.api.common.DeleteResponse. - core-entities: DeleteResponse -> EntityDeleteResponse (keeps deleted_count), updating all four delete endpoints. - core-entities GenericSortField -> WorkspaceSortField; guardrails GenericSortField -> GuardrailConfigSortField. Regenerates openapi/openapi.yaml and the ga/ merged specs. Refs AALGO-352. Signed-off-by: Albert Cui --- openapi/ga/individual/platform.openapi.yaml | 47 ++++++++++++++++--- openapi/ga/openapi.yaml | 47 ++++++++++++++++--- openapi/openapi.yaml | 47 ++++++++++++++++--- .../entities/api/v2/entities/endpoints.py | 8 ++-- .../entities/api/v2/projects/endpoints.py | 8 ++-- .../src/nmp/core/entities/api/v2/schemas.py | 4 +- .../entities/api/v2/workspaces/endpoints.py | 16 +++---- .../guardrails/api/v2/configs/endpoints.py | 6 +-- .../src/nmp/guardrails/app/common/common.py | 10 +--- 9 files changed, 145 insertions(+), 48 deletions(-) diff --git a/openapi/ga/individual/platform.openapi.yaml b/openapi/ga/individual/platform.openapi.yaml index 6d3526b18f..2a04ecbe75 100644 --- a/openapi/ga/individual/platform.openapi.yaml +++ b/openapi/ga/individual/platform.openapi.yaml @@ -414,7 +414,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GenericSortField' + - $ref: '#/components/schemas/WorkspaceSortField' description: Sort field default: -created_at description: Sort field @@ -566,7 +566,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -899,7 +899,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -1100,7 +1100,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -1359,7 +1359,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -2002,7 +2002,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GenericSortField' + - $ref: '#/components/schemas/GuardrailConfigSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: created_at @@ -10383,6 +10383,26 @@ components: Name is optional - if not provided, it will be auto-generated. Workspace and entity_type come from the URL path parameters.' + EntityDeleteResponse: + properties: + message: + type: string + title: Message + default: Resource deleted successfully + id: + type: string + title: Id + description: ID of the deleted resource + deleted_count: + type: integer + title: Deleted Count + description: Number of items deleted + default: 1 + type: object + required: + - id + title: EntityDeleteResponse + description: Response for successful delete operations. EntityUpdate: properties: new_name: @@ -12300,6 +12320,12 @@ components: - name title: GuardrailConfigInput description: Input schema for creating a guardrail config. + GuardrailConfigSortField: + type: string + enum: + - created_at + - -created_at + title: GuardrailConfigSortField GuardrailConfigUpdate: properties: description: @@ -19626,6 +19652,15 @@ components: - roles title: WorkspaceMemberUpdate description: Schema for updating a workspace member's roles. + WorkspaceSortField: + type: string + enum: + - created_at + - -created_at + - updated_at + - -updated_at + title: WorkspaceSortField + description: Fields available for sorting workspace results. WorkspaceUpdate: properties: description: diff --git a/openapi/ga/openapi.yaml b/openapi/ga/openapi.yaml index 6d3526b18f..2a04ecbe75 100644 --- a/openapi/ga/openapi.yaml +++ b/openapi/ga/openapi.yaml @@ -414,7 +414,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GenericSortField' + - $ref: '#/components/schemas/WorkspaceSortField' description: Sort field default: -created_at description: Sort field @@ -566,7 +566,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -899,7 +899,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -1100,7 +1100,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -1359,7 +1359,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -2002,7 +2002,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GenericSortField' + - $ref: '#/components/schemas/GuardrailConfigSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: created_at @@ -10383,6 +10383,26 @@ components: Name is optional - if not provided, it will be auto-generated. Workspace and entity_type come from the URL path parameters.' + EntityDeleteResponse: + properties: + message: + type: string + title: Message + default: Resource deleted successfully + id: + type: string + title: Id + description: ID of the deleted resource + deleted_count: + type: integer + title: Deleted Count + description: Number of items deleted + default: 1 + type: object + required: + - id + title: EntityDeleteResponse + description: Response for successful delete operations. EntityUpdate: properties: new_name: @@ -12300,6 +12320,12 @@ components: - name title: GuardrailConfigInput description: Input schema for creating a guardrail config. + GuardrailConfigSortField: + type: string + enum: + - created_at + - -created_at + title: GuardrailConfigSortField GuardrailConfigUpdate: properties: description: @@ -19626,6 +19652,15 @@ components: - roles title: WorkspaceMemberUpdate description: Schema for updating a workspace member's roles. + WorkspaceSortField: + type: string + enum: + - created_at + - -created_at + - updated_at + - -updated_at + title: WorkspaceSortField + description: Fields available for sorting workspace results. WorkspaceUpdate: properties: description: diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 6d3526b18f..2a04ecbe75 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -414,7 +414,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GenericSortField' + - $ref: '#/components/schemas/WorkspaceSortField' description: Sort field default: -created_at description: Sort field @@ -566,7 +566,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -899,7 +899,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -1100,7 +1100,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -1359,7 +1359,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -2002,7 +2002,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GenericSortField' + - $ref: '#/components/schemas/GuardrailConfigSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: created_at @@ -10383,6 +10383,26 @@ components: Name is optional - if not provided, it will be auto-generated. Workspace and entity_type come from the URL path parameters.' + EntityDeleteResponse: + properties: + message: + type: string + title: Message + default: Resource deleted successfully + id: + type: string + title: Id + description: ID of the deleted resource + deleted_count: + type: integer + title: Deleted Count + description: Number of items deleted + default: 1 + type: object + required: + - id + title: EntityDeleteResponse + description: Response for successful delete operations. EntityUpdate: properties: new_name: @@ -12300,6 +12320,12 @@ components: - name title: GuardrailConfigInput description: Input schema for creating a guardrail config. + GuardrailConfigSortField: + type: string + enum: + - created_at + - -created_at + title: GuardrailConfigSortField GuardrailConfigUpdate: properties: description: @@ -19626,6 +19652,15 @@ components: - roles title: WorkspaceMemberUpdate description: Schema for updating a workspace member's roles. + WorkspaceSortField: + type: string + enum: + - created_at + - -created_at + - updated_at + - -updated_at + title: WorkspaceSortField + description: Fields available for sorting workspace results. WorkspaceUpdate: properties: description: diff --git a/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py b/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py index cfccc6be3e..a766289eb8 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py @@ -31,7 +31,7 @@ WorkspaceRepository, ) from nmp.core.entities.api.v2.entities.schemas import EntityCreateInput, EntityUpdate -from nmp.core.entities.api.v2.schemas import DeleteResponse +from nmp.core.entities.api.v2.schemas import EntityDeleteResponse from nmp.core.entities.api.v2.utils import ( ROLE_BINDING_ENTITY_TYPE, add_workspace_filtering, @@ -561,7 +561,7 @@ async def update_entity_by_name( @router.delete( "/v2/workspaces/{workspace}/entities/{entity_type}/{name}", - response_model=DeleteResponse, + response_model=EntityDeleteResponse, tags=[API_TAG], summary="Delete entity by name", description=textwrap.dedent(""" @@ -581,7 +581,7 @@ async def delete_entity_by_name( workspace_repository: WorkspaceRepository, auth_client: AuthClientDep, parent: str | None = Query(default=None, description="Parent entity ID for nested entities"), -) -> DeleteResponse: +) -> EntityDeleteResponse: """Delete entity by name.""" # Check if workspace is being deleted (404 for user requests) await validate_workspace_not_deleting(workspace_repository, auth_client, workspace) @@ -601,7 +601,7 @@ async def delete_entity_by_name( ) if deleted_count == 0: raise HTTPException(status_code=404, detail="Entity not found") - return DeleteResponse(id=f"{workspace}/{entity_type}/{name}", deleted_count=deleted_count) + return EntityDeleteResponse(id=f"{workspace}/{entity_type}/{name}", deleted_count=deleted_count) @router.get( diff --git a/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py b/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py index 4849fe053f..28b4aff883 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py @@ -24,7 +24,7 @@ ProjectSortField, ProjectUpdate, ) -from nmp.core.entities.api.v2.schemas import DeleteResponse +from nmp.core.entities.api.v2.schemas import EntityDeleteResponse from nmp.core.entities.app.repository.exceptions import ( EntityNotFoundError, EntityVersionConflictError, @@ -255,7 +255,7 @@ async def update_project( @router.delete( "/v2/workspaces/{workspace}/projects/{name}", - response_model=DeleteResponse, + response_model=EntityDeleteResponse, tags=[API_TAG], summary="Delete project", description=textwrap.dedent(""" @@ -271,7 +271,7 @@ async def delete_project( workspace: str, name: str, repository: EntityRepository, -) -> DeleteResponse: +) -> EntityDeleteResponse: """Delete project.""" try: deleted_count = await repository.delete_entity_by_name( @@ -284,7 +284,7 @@ async def delete_project( status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found in workspace '{workspace}'", ) - return DeleteResponse( + return EntityDeleteResponse( id=f"{workspace}/{name}", message="Project deleted successfully", deleted_count=deleted_count, diff --git a/services/core/entities/src/nmp/core/entities/api/v2/schemas.py b/services/core/entities/src/nmp/core/entities/api/v2/schemas.py index e919e65c65..7fa4b67b9f 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/schemas.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/schemas.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, Field -class GenericSortField(StrEnum): +class WorkspaceSortField(StrEnum): """Fields available for sorting workspace results.""" CREATED_AT_ASC = "created_at" @@ -17,7 +17,7 @@ class GenericSortField(StrEnum): UPDATED_AT_DESC = "-updated_at" -class DeleteResponse(BaseModel): +class EntityDeleteResponse(BaseModel): """Response for successful delete operations.""" message: str = Field(default="Resource deleted successfully") diff --git a/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py b/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py index addb788ba7..06f38569ea 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py @@ -28,7 +28,7 @@ from nmp.common.api.filter import ComparisonOperation, FilterOperator from nmp.common.auth.models import Principal from nmp.core.entities.api.dependencies import AuthClientDep, EntityRepository, WorkspaceRepository -from nmp.core.entities.api.v2.schemas import DeleteResponse, GenericSortField +from nmp.core.entities.api.v2.schemas import EntityDeleteResponse, WorkspaceSortField from nmp.core.entities.api.v2.utils import ( ROLE_BINDING_ENTITY_TYPE, add_workspace_filtering, @@ -286,7 +286,7 @@ async def list_workspaces( filter: FilterDep, page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(100, ge=1, le=1000, description="Items per page"), - sort: GenericSortField = Query(GenericSortField.CREATED_AT_DESC, description="Sort field"), + sort: WorkspaceSortField = Query(WorkspaceSortField.CREATED_AT_DESC, description="Sort field"), ) -> Page[Workspace]: """List workspaces accessible to the current principal.""" # Get accessible workspaces for access control @@ -395,7 +395,7 @@ async def update_workspace( @router.delete( "/v2/workspaces/{name}", - response_model=DeleteResponse, + response_model=EntityDeleteResponse, tags=[API_TAG], summary="Delete workspace", description=textwrap.dedent(""" @@ -417,7 +417,7 @@ async def delete_workspace( name: str, repository: WorkspaceRepository, entity_repository: EntityRepository, -) -> DeleteResponse: +) -> EntityDeleteResponse: """Mark workspace for deletion.""" # Check if workspace exists first workspace = await repository.get_workspace_by_name(name=name) @@ -456,7 +456,7 @@ async def delete_workspace( extra={"workspace": name, "deleted_count": len(deleted_bindings)}, ) - return DeleteResponse( + return EntityDeleteResponse( id=name, message="Workspace marked for deletion", deleted_count=1, @@ -818,7 +818,7 @@ async def update_workspace_member( @router.delete( "/v2/workspaces/{workspace}/members/{principal_id}", - response_model=DeleteResponse, + response_model=EntityDeleteResponse, tags=[API_TAG], summary="Remove workspace member", description=textwrap.dedent(""" @@ -844,7 +844,7 @@ async def remove_workspace_member( default=True, description="If true, wait for roles to propagate before returning (default: true). Set to false for bulk operations.", ), -) -> DeleteResponse: +) -> EntityDeleteResponse: """Remove a member from the workspace.""" # Check if workspace exists ws = await workspace_repository.get_workspace_by_name(name=workspace) @@ -920,7 +920,7 @@ async def remove_workspace_member( extra={"workspace": workspace, "principal": principal_id, "role": role}, ) - return DeleteResponse( + return EntityDeleteResponse( id=principal_id, message=f"Member removed from workspace '{workspace}'", deleted_count=len(active_bindings), diff --git a/services/guardrails/src/nmp/guardrails/api/v2/configs/endpoints.py b/services/guardrails/src/nmp/guardrails/api/v2/configs/endpoints.py index 871cd49328..0074979387 100644 --- a/services/guardrails/src/nmp/guardrails/api/v2/configs/endpoints.py +++ b/services/guardrails/src/nmp/guardrails/api/v2/configs/endpoints.py @@ -7,13 +7,13 @@ from fastapi import APIRouter, Depends, HTTPException, Query from nmp.common.api import ParsedFilter, make_filter_dep -from nmp.common.api.common import Page, PaginationData +from nmp.common.api.common import DeleteResponse, Page, PaginationData from nmp.common.api.utils import generate_openapi_extra_params from nmp.common.entities import EntityClient, EntityConflictError, EntityNotFoundError from nmp.common.service.dependencies import get_entity_client from nmp.guardrails.api.dependencies import ConfigRegistryDep, RailsRegistryDep from nmp.guardrails.api.v2.configs.schemas import GuardrailConfigFilter, GuardrailConfigInput, GuardrailConfigUpdate -from nmp.guardrails.app.common.common import DeleteResponse, GenericSortField +from nmp.guardrails.app.common.common import GuardrailConfigSortField from nmp.guardrails.app.utils.config_utils import enrich_config_with_data, invalidate_and_reload_config_cache from nmp.guardrails.entities import GuardrailConfig @@ -36,7 +36,7 @@ async def list_guardrail_configs( entities_client: EntityClient = Depends(get_entity_client), page: int = Query(default=1, description="Page number."), page_size: int = Query(default=10, description="Page size."), - sort: GenericSortField = Query( + sort: GuardrailConfigSortField = Query( default="created_at", description="""The field to sort by. To sort in decreasing order, use `-` in front of the field name.""", ), diff --git a/services/guardrails/src/nmp/guardrails/app/common/common.py b/services/guardrails/src/nmp/guardrails/app/common/common.py index cee1643dc1..4e8e620d34 100644 --- a/services/guardrails/src/nmp/guardrails/app/common/common.py +++ b/services/guardrails/src/nmp/guardrails/app/common/common.py @@ -3,9 +3,7 @@ """Common structures used across multiple API endpoints/schemas.""" -from datetime import datetime from enum import Enum, StrEnum -from typing import Optional from nmp.common.entities.values import Value from pydantic import BaseModel, Field @@ -22,17 +20,11 @@ class URN(str): """ -class GenericSortField(StrEnum): +class GuardrailConfigSortField(StrEnum): CREATED_AT_ASC = "created_at" CREATED_AT_DESC = "-created_at" -class DeleteResponse(Value): - message: str = Field(default="Resource deleted successfully.") - id: Optional[str] = Field(default=None, description="The ID of the deleted resource.") - deleted_at: Optional[datetime] = Field(default=None, description="The timestamp when the resource was deleted.") - - class ErrorResponse(Value): detail: str = Field(json_schema_extra={"example": "Error message"}) From b2aa15eeb24790bfbb1364b9077dc79c0d1d18a8 Mon Sep 17 00:00:00 2001 From: Albert Cui Date: Fri, 17 Jul 2026 22:47:18 -0400 Subject: [PATCH 2/9] chore(openapi): make schema-collision detection strict by default The differing-content collision gate added in #737 was opt-in: only the nemo-customizer plugin spec enforced it, while platform/service specs stayed on warn-and-collapse. That is exactly how the AALGO-352 collisions shipped silently. With those fixed (zero collisions across the platform, all services, and all plugins), flip the default so any future collision fails spec generation loudly instead of shipping a wrong SDK contract: - tweak_spec, apply_schema_fixes, and PluginConfig.strict_schema_collisions default to True. The plugin "lenient" branch now passes strict_collisions=False explicitly (it previously relied on the old default). - A spec can still opt out via strict_collisions=False / [tool.nemo.openapi].strict_schema_collisions = false. - Generalize the collision error message (rename/dedupe or namespace) and invert the unit tests to pin the policy: default raises, opt-out warns. Enforcement is at spec-generation time (make refresh-openapi / the manual openapi-generator pre-commit hook). CI does not regenerate the spec today, so this does not yet gate PRs on its own; adding a CI regen/drift check is a follow-up. Refs AALGO-352. Signed-off-by: Albert Cui --- .../nmp_common/src/nmp/common/api/utils.py | 25 ++++++++++--------- .../tests/api/test_utils_openapi_spec.py | 20 +++++++-------- plugins/nemo-customizer/pyproject.toml | 5 ++-- script/generate_openapi_spec.py | 13 ++++------ script/openapi_helper/plugin_config.py | 13 +++++----- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/packages/nmp_common/src/nmp/common/api/utils.py b/packages/nmp_common/src/nmp/common/api/utils.py index 518d66d9a8..8664adc1e2 100644 --- a/packages/nmp_common/src/nmp/common/api/utils.py +++ b/packages/nmp_common/src/nmp/common/api/utils.py @@ -148,7 +148,7 @@ def _walk_spec(d: Dict, visitor: Callable[[str, Any, Dict], None]): _walk_spec(item, visitor) -def _normalize_refs_and_schema_keys(spec: Dict, *, strict_collisions: bool = False) -> Dict: +def _normalize_refs_and_schema_keys(spec: Dict, *, strict_collisions: bool = True) -> Dict: """Normalize all ``$ref`` values and schema dictionary keys. Schema keys are renamed first so that ``$ref`` values can be rewritten @@ -157,13 +157,12 @@ def _normalize_refs_and_schema_keys(spec: Dict, *, strict_collisions: bool = Fal When the content *differs*, two distinct Pydantic models are fighting over one schema name, and keeping one silently makes the other's ``$ref``\\ s point - at the wrong contract. With ``strict_collisions`` this raises ``ValueError`` - so the build fails loudly — used for self-contained plugin specs (e.g. the - merged ``/apis/customization`` app, where each backend must namespace its - own models). Without it (the default, used for the platform/service specs) - it logs a warning and keeps the first-seen schema, preserving legacy - behaviour: the platform spec carries pre-existing such collisions that - predate this gate and are tracked separately. + at the wrong contract. ``strict_collisions`` (the default) raises + ``ValueError`` so the build fails loudly rather than shipping a wrong + contract in the generated SDK. Pass ``strict_collisions=False`` to opt a + spec out — it then logs a warning and keeps the first-seen schema (legacy + warn-and-collapse), for a spec that must tolerate a known pre-existing + collision. """ schemas = spec["components"]["schemas"] @@ -189,9 +188,11 @@ def _normalize_refs_and_schema_keys(spec: Dict, *, strict_collisions: bool = Fal if len(reps) > 1: message = ( f"OpenAPI schema name collision: {sorted(old_keys)} all normalize to " - f"'{target}' with differing content. Two distinct Pydantic models share " - f"a class name across modules — namespace them (e.g. via a per-backend " - f"NamespacedModel base) so they emit distinct schema names." + f"'{target}' with differing content. Two distinct Pydantic models share a " + f"class name across modules; give them distinct names — rename or dedupe the " + f"models, or namespace them (e.g. via a per-backend NamespacedModel base) — so " + f"they emit distinct schema names. A spec that must tolerate the collision can " + f"opt out with strict_collisions=False." ) if strict_collisions: raise ValueError(message) @@ -355,7 +356,7 @@ def _sort_schemas(spec: Dict) -> Dict: return spec -def tweak_spec(spec: Dict, *, strict_collisions: bool = False) -> Dict: +def tweak_spec(spec: Dict, *, strict_collisions: bool = True) -> Dict: _walk_spec(spec, _anyof_null_visitor) spec = _normalize_refs_and_schema_keys(spec, strict_collisions=strict_collisions) spec = _split_input_output_schemas(spec) diff --git a/packages/nmp_common/tests/api/test_utils_openapi_spec.py b/packages/nmp_common/tests/api/test_utils_openapi_spec.py index 77c15e43f4..e05cef4a39 100644 --- a/packages/nmp_common/tests/api/test_utils_openapi_spec.py +++ b/packages/nmp_common/tests/api/test_utils_openapi_spec.py @@ -356,22 +356,22 @@ def _collision_spec(): } -def test_tweak_spec_raises_on_collision_when_strict(): - """With ``strict_collisions`` (plugin specs, e.g. the customization app) a - differing-content collision must fail the build loudly rather than silently - keeping one and mis-pointing the other's ``$ref``s.""" +def test_tweak_spec_raises_on_collision_by_default(): + """A differing-content schema-name collision fails the build by default, + rather than silently keeping one model and mis-pointing the other's + ``$ref``s (which would ship a wrong contract in the generated SDK).""" with pytest.raises(ValueError, match="schema name collision"): - tweak_spec(_collision_spec(), strict_collisions=True) + tweak_spec(_collision_spec()) -def test_tweak_spec_warns_and_collapses_on_collision_by_default(caplog): - """Non-strict (platform/service specs) preserves legacy behaviour: warn and - keep the first-seen schema, so pre-existing platform collisions don't newly - break the build.""" +def test_tweak_spec_warns_and_collapses_when_opted_out(caplog): + """``strict_collisions=False`` opts a spec out: warn and keep the first-seen + schema (legacy warn-and-collapse) for a spec that must tolerate a known + pre-existing collision.""" import logging with caplog.at_level(logging.WARNING, logger="nmp.common.api.utils"): - result = tweak_spec(_collision_spec()) + result = tweak_spec(_collision_spec(), strict_collisions=False) assert "schema name collision" in caplog.text schemas = result["components"]["schemas"] diff --git a/plugins/nemo-customizer/pyproject.toml b/plugins/nemo-customizer/pyproject.toml index bc0f0aa564..083f639be1 100644 --- a/plugins/nemo-customizer/pyproject.toml +++ b/plugins/nemo-customizer/pyproject.toml @@ -33,9 +33,10 @@ build-backend = "hatchling.build" packages = ["src/nemo_customizer"] [tool.nemo.openapi] +# Strict collision detection is the platform-wide default; pinned here because # nemo-customizer merges every customization backend (automodel, unsloth, …) -# into one /apis/customization app, so enforce that no two backends emit the -# same OpenAPI schema name with differing content (see NamespacedModel). +# into one /apis/customization app, where two backends emitting the same schema +# name with differing content is always a bug (see NamespacedModel). strict_schema_collisions = true diff --git a/script/generate_openapi_spec.py b/script/generate_openapi_spec.py index 5b190d5184..141a96be75 100644 --- a/script/generate_openapi_spec.py +++ b/script/generate_openapi_spec.py @@ -466,7 +466,7 @@ def extract_plugin_specs_with_process_pool(plugins: List[PluginConfig]) -> None: print_green(f"All {len(plugins)} plugin(s) completed successfully!") -def apply_schema_fixes(spec_files: List[str], apply_reorder: bool = True, strict_collisions: bool = False) -> None: +def apply_schema_fixes(spec_files: List[str], apply_reorder: bool = True, strict_collisions: bool = True) -> None: """Apply schema fixes to a list of OpenAPI spec files.""" print_green("=== Applying fixes to OpenAPI schemas ===") # Endpoints stripped from the public OpenAPI spec (not exposed in SDK). @@ -748,17 +748,14 @@ def process_plugin_specs() -> None: # gated on `"platform" in spec_file` are no-ops because plugin paths live # under plugins//openapi/ — no false positives by inspection. # - # A plugin opts into strict_collisions via - # [tool.nemo.openapi].strict_schema_collisions when its spec merges multiple - # sub-apps (e.g. nemo-customizer, which mounts every customization backend - # under /apis/customization): there, two backends defining a same-named - # model with differing content is always a bug, so fail the build loudly - # instead of silently shipping a wrong contract. Plugin specs are + # Schema-name collisions fail the build by default (strict). A plugin may + # opt OUT via [tool.nemo.openapi].strict_schema_collisions = false if its + # spec must tolerate a known pre-existing collision. Plugin specs are # independent files, so processing the two groups separately is safe. strict_spec_files = [p.output_path() for p in plugins if p.strict_schema_collisions] lenient_spec_files = [p.output_path() for p in plugins if not p.strict_schema_collisions] if lenient_spec_files: - apply_schema_fixes(lenient_spec_files) + apply_schema_fixes(lenient_spec_files, strict_collisions=False) if strict_spec_files: apply_schema_fixes(strict_spec_files, strict_collisions=True) fix_ref_not_allowed_errors(plugin_spec_files) diff --git a/script/openapi_helper/plugin_config.py b/script/openapi_helper/plugin_config.py index e4057c093c..36816135dd 100644 --- a/script/openapi_helper/plugin_config.py +++ b/script/openapi_helper/plugin_config.py @@ -22,12 +22,11 @@ class PluginConfig: env_vars: Optional[Dict[str, str]] = None factory_override: Optional[str] = None # "module:callable" escape hatch data_designer_plugin_allowlist: Optional[List[str]] = None - # Opt in when the plugin's spec merges multiple sub-apps into one (e.g. - # nemo-customizer mounts every customization backend under - # /apis/customization). Then two backends defining a same-named model with - # differing content is always a real bug, and spec generation should fail - # loudly instead of silently collapsing them. Off => warn-and-collapse. - strict_schema_collisions: bool = False + # Schema-name collisions fail spec generation by default. Set to false to + # opt a plugin out (warn-and-collapse) if its spec must tolerate a known + # pre-existing collision — collapsing silently ships a wrong contract, so + # opting out should be rare and deliberate. + strict_schema_collisions: bool = True @classmethod def from_pyproject(cls, pyproject_path: Path) -> Optional["PluginConfig"]: @@ -55,7 +54,7 @@ def from_pyproject(cls, pyproject_path: Path) -> Optional["PluginConfig"]: raise ValueError( f"plugin '{plugin_dir}': [tool.nemo.openapi].data_designer_plugin_allowlist must be a list of strings" ) - strict_schema_collisions = opts.get("strict_schema_collisions", False) + strict_schema_collisions = opts.get("strict_schema_collisions", True) if not isinstance(strict_schema_collisions, bool): raise ValueError(f"plugin '{plugin_dir}': [tool.nemo.openapi].strict_schema_collisions must be a boolean") From 284588cf18b40b13b240782b2b3d0a8e703f0bcd Mon Sep 17 00:00:00 2001 From: Albert Cui Date: Fri, 17 Jul 2026 23:46:52 -0400 Subject: [PATCH 3/9] chore(sdk): regenerate Python SDK for renamed schemas Regenerate the vendored Python SDK (`make update-sdk`) to pick up the schema renames from the AALGO-352 fix. Adds EntityDeleteResponse, WorkspaceSortField, and GuardrailConfigSortField: the entities/projects/workspaces delete endpoints now return EntityDeleteResponse, and the workspaces / guardrail-config list sort params reference the new enums. The shared DeleteResponse / GenericSortField are unchanged (still used by iam role-bindings, files, etc.). The Stainless mapping report was warning-free; the three new models auto-mapped correctly (entity_delete_response under $shared since it is returned by three resources; the two sort fields under their own resources). Also hand-update packages/nemo_platform_plugin/entities.py: its delete() / delete_by_id() wrapper methods forward AsyncEntitiesResource.delete_entity_by_name, whose return type is now EntityDeleteResponse, so retype those annotations. Regen does not touch this hand-written file. (Pre-existing `parent: str | Omit` ty diagnostics in that file are unrelated and left as-is.) Refs AALGO-352. Signed-off-by: Albert Cui --- .../src/nemo_platform_plugin/entities.py | 10 ++-- .../nemo-platform/.nmpcontext/openapi.yaml | 47 ++++++++++++++++--- .../nemo-platform/.nmpcontext/stainless.yaml | 3 ++ sdk/python/nemo-platform/api.md | 1 + .../nemo_platform/resources/entities/api.md | 2 +- .../resources/entities/entities.py | 10 ++-- .../nemo_platform/resources/guardrail/api.md | 1 + .../resources/guardrail/configs.py | 13 +++-- .../nemo_platform/resources/projects/api.md | 2 +- .../resources/projects/projects.py | 10 ++-- .../nemo_platform/resources/workspaces/api.md | 5 +- .../resources/workspaces/members.py | 10 ++-- .../resources/workspaces/workspaces.py | 23 +++++---- .../src/nemo_platform/types/__init__.py | 1 + .../nemo_platform/types/guardrail/__init__.py | 1 + .../types/guardrail/config_list_params.py | 4 +- .../guardrail/guardrail_config_sort_field.py | 22 +++++++++ .../nemo_platform/types/shared/__init__.py | 1 + .../types/shared/entity_delete_response.py | 34 ++++++++++++++ .../types/workspaces/__init__.py | 1 + .../types/workspaces/workspace_list_params.py | 4 +- .../types/workspaces/workspace_sort_field.py | 22 +++++++++ .../tests/api_resources/test_entities.py | 18 +++---- .../tests/api_resources/test_projects.py | 14 +++--- .../tests/api_resources/test_workspaces.py | 14 +++--- .../api_resources/workspaces/test_members.py | 18 +++---- sdk/stainless.yaml | 3 ++ 27 files changed, 215 insertions(+), 79 deletions(-) create mode 100644 sdk/python/nemo-platform/src/nemo_platform/types/guardrail/guardrail_config_sort_field.py create mode 100644 sdk/python/nemo-platform/src/nemo_platform/types/shared/entity_delete_response.py create mode 100644 sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_sort_field.py diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py index 56dde5bd12..b51e211d3b 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py @@ -10,7 +10,7 @@ from nemo_platform import ConflictError, NotFoundError, UnprocessableEntityError, omit from nemo_platform.resources.entities import AsyncEntitiesResource -from nemo_platform.types import DeleteResponse +from nemo_platform.types import EntityDeleteResponse from nemo_platform.types.entities import Entity from nemo_platform_plugin.filter_ops import FilterOperation from pydantic import BaseModel, Field, PrivateAttr, TypeAdapter, computed_field @@ -238,8 +238,8 @@ async def get_by_id(self, entity_type: EntityTypeLike, entity_id: str) -> Entity async def update(self, entity: EntityT, *, original_name: str | None = None) -> EntityT: ... async def delete( self, entity_type: EntityTypeLike, name: str, *, workspace: Optional[str] = None - ) -> DeleteResponse: ... - async def delete_by_id(self, entity_type: EntityTypeLike, entity_id: str) -> DeleteResponse: ... + ) -> EntityDeleteResponse: ... + async def delete_by_id(self, entity_type: EntityTypeLike, entity_id: str) -> EntityDeleteResponse: ... async def save(self, entity: EntityT) -> EntityT: ... async def add(self, entity: EntityT) -> EntityT: ... async def get_by_field( @@ -633,7 +633,7 @@ async def delete( *, workspace: Optional[str] = None, parent: Optional[str] = None, - ) -> DeleteResponse: + ) -> EntityDeleteResponse: """Delete an entity by name. Supports workspace-qualified names like "prod/my-model". @@ -665,7 +665,7 @@ async def delete_by_id( self, entity_type: EntityTypeLike, entity_id: str, - ) -> DeleteResponse: + ) -> EntityDeleteResponse: """Delete an entity by ID. First retrieves the entity to get its workspace and name, then deletes by name. diff --git a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml index 6d3526b18f..2a04ecbe75 100644 --- a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml @@ -414,7 +414,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GenericSortField' + - $ref: '#/components/schemas/WorkspaceSortField' description: Sort field default: -created_at description: Sort field @@ -566,7 +566,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -899,7 +899,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -1100,7 +1100,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -1359,7 +1359,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/EntityDeleteResponse' '422': description: Validation Error content: @@ -2002,7 +2002,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GenericSortField' + - $ref: '#/components/schemas/GuardrailConfigSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: created_at @@ -10383,6 +10383,26 @@ components: Name is optional - if not provided, it will be auto-generated. Workspace and entity_type come from the URL path parameters.' + EntityDeleteResponse: + properties: + message: + type: string + title: Message + default: Resource deleted successfully + id: + type: string + title: Id + description: ID of the deleted resource + deleted_count: + type: integer + title: Deleted Count + description: Number of items deleted + default: 1 + type: object + required: + - id + title: EntityDeleteResponse + description: Response for successful delete operations. EntityUpdate: properties: new_name: @@ -12300,6 +12320,12 @@ components: - name title: GuardrailConfigInput description: Input schema for creating a guardrail config. + GuardrailConfigSortField: + type: string + enum: + - created_at + - -created_at + title: GuardrailConfigSortField GuardrailConfigUpdate: properties: description: @@ -19626,6 +19652,15 @@ components: - roles title: WorkspaceMemberUpdate description: Schema for updating a workspace member's roles. + WorkspaceSortField: + type: string + enum: + - created_at + - -created_at + - updated_at + - -updated_at + title: WorkspaceSortField + description: Fields available for sorting workspace results. WorkspaceUpdate: properties: description: diff --git a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml index 76037a1354..08432deefa 100644 --- a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml @@ -368,6 +368,7 @@ resources: models: guardrail_config_filter: GuardrailConfigFilter guardrail_config_param: GuardrailConfigInput + guardrail_config_sort_field: GuardrailConfigSortField guardrail_config_update: GuardrailConfigUpdate guardrail_configs_page: GuardrailConfigsPage methods: @@ -648,6 +649,7 @@ resources: models: workspace: Workspace workspace_param: WorkspaceInput + workspace_sort_field: WorkspaceSortField workspace_update: WorkspaceUpdate workspaces_page: WorkspacesPage methods: @@ -751,6 +753,7 @@ resources: workload_token_exchange_error_response: WorkloadTokenExchangeErrorResponse json_web_key: JsonWebKey json_web_key_set_response: JsonWebKeySetResponse + entity_delete_response: EntityDeleteResponse iam: standalone_api: true subresources: diff --git a/sdk/python/nemo-platform/api.md b/sdk/python/nemo-platform/api.md index b235448956..00c028782a 100644 --- a/sdk/python/nemo-platform/api.md +++ b/sdk/python/nemo-platform/api.md @@ -9,6 +9,7 @@ from nemo_platform.types import ( DatasetMetadataContent, DatetimeFilter, DeleteResponse, + EntityDeleteResponse, FileStorageType, FilesetMetadata, FilesetMetadataParam, diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/entities/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/entities/api.md index 79b770c00c..86b88c8b92 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/entities/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/entities/api.md @@ -10,7 +10,7 @@ Methods: - client.entities.create(entity_type, \*, workspace, \*\*params) -> Entity - client.entities.list(entity_type, \*, workspace, \*\*params) -> SyncDefaultPagination[Entity] -- client.entities.delete_entity_by_name(name, \*, workspace, entity_type, \*\*params) -> DeleteResponse +- client.entities.delete_entity_by_name(name, \*, workspace, entity_type, \*\*params) -> EntityDeleteResponse - client.entities.get_entity_by_id(id) -> Entity - client.entities.get_entity_by_name(name, \*, workspace, entity_type, \*\*params) -> Entity - client.entities.update_entity_by_name(name, \*, workspace, entity_type, \*\*params) -> Entity diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/entities/entities.py b/sdk/python/nemo-platform/src/nemo_platform/resources/entities/entities.py index 3375494483..99902e5805 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/entities/entities.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/entities/entities.py @@ -41,7 +41,7 @@ entity_update_entity_by_name_params, ) from ...types.entities.entity import Entity -from ...types.shared.delete_response import DeleteResponse +from ...types.shared.entity_delete_response import EntityDeleteResponse __all__ = ["EntitiesResource", "AsyncEntitiesResource"] @@ -245,7 +245,7 @@ def delete_entity_by_name( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DeleteResponse: + ) -> EntityDeleteResponse: """ Delete an entity by its name. @@ -290,7 +290,7 @@ def delete_entity_by_name( {"parent": parent}, entity_delete_entity_by_name_params.EntityDeleteEntityByNameParams ), ), - cast_to=DeleteResponse, + cast_to=EntityDeleteResponse, ) def get_entity_by_id( @@ -680,7 +680,7 @@ async def delete_entity_by_name( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DeleteResponse: + ) -> EntityDeleteResponse: """ Delete an entity by its name. @@ -725,7 +725,7 @@ async def delete_entity_by_name( {"parent": parent}, entity_delete_entity_by_name_params.EntityDeleteEntityByNameParams ), ), - cast_to=DeleteResponse, + cast_to=EntityDeleteResponse, ) async def get_entity_by_id( diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md index 29e61b6bd4..c20d87c7cd 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md @@ -107,6 +107,7 @@ Types: from nemo_platform.types.guardrail import ( GuardrailConfigFilter, GuardrailConfigParam, + GuardrailConfigSortField, GuardrailConfigUpdate, GuardrailConfigsPage, ) diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/configs.py b/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/configs.py index 28b444f882..d3715c9b81 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/configs.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/configs.py @@ -33,10 +33,15 @@ ) from ...pagination import SyncDefaultPagination, AsyncDefaultPagination from ..._base_client import AsyncPaginator, make_request_options -from ...types.guardrail import config_list_params, config_create_params, config_update_params +from ...types.guardrail import ( + GuardrailConfigSortField, + config_list_params, + config_create_params, + config_update_params, +) from ...types.shared.delete_response import DeleteResponse -from ...types.shared.generic_sort_field import GenericSortField from ...types.guardrail.guardrail_config import GuardrailConfig +from ...types.guardrail.guardrail_config_sort_field import GuardrailConfigSortField from ...types.guardrail.guardrail_config_filter_param import GuardrailConfigFilterParam from ..._exceptions import ConflictError @@ -223,7 +228,7 @@ def list( filter: GuardrailConfigFilterParam | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: GenericSortField | Omit = omit, + sort: GuardrailConfigSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -499,7 +504,7 @@ def list( filter: GuardrailConfigFilterParam | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: GenericSortField | Omit = omit, + sort: GuardrailConfigSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md index 1dee2157bc..213f17faf5 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md @@ -18,4 +18,4 @@ Methods: - client.projects.retrieve(name, \*, workspace) -> Project - client.projects.update(name, \*, workspace, \*\*params) -> Project - client.projects.list(\*, workspace, \*\*params) -> SyncDefaultPagination[Project] -- client.projects.delete(name, \*, workspace) -> DeleteResponse +- client.projects.delete(name, \*, workspace) -> EntityDeleteResponse diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py index a16219062a..ab7eff8672 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py @@ -33,8 +33,8 @@ from ..._base_client import AsyncPaginator, make_request_options from ...types.projects import ProjectSortField, project_list_params, project_create_params, project_update_params from ...types.projects.project import Project -from ...types.shared.delete_response import DeleteResponse from ...types.projects.project_sort_field import ProjectSortField +from ...types.shared.entity_delete_response import EntityDeleteResponse from ..._exceptions import ConflictError __all__ = ["ProjectsResource", "AsyncProjectsResource"] @@ -312,7 +312,7 @@ def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DeleteResponse: + ) -> EntityDeleteResponse: """ Delete a project. @@ -342,7 +342,7 @@ def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=DeleteResponse, + cast_to=EntityDeleteResponse, ) @@ -618,7 +618,7 @@ async def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DeleteResponse: + ) -> EntityDeleteResponse: """ Delete a project. @@ -648,7 +648,7 @@ async def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=DeleteResponse, + cast_to=EntityDeleteResponse, ) diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md index bf835b3c96..acd2281a32 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md @@ -6,6 +6,7 @@ Types: from nemo_platform.types.workspaces import ( Workspace, WorkspaceParam, + WorkspaceSortField, WorkspaceUpdate, WorkspacesPage, ) @@ -17,7 +18,7 @@ Methods: - client.workspaces.retrieve(name) -> Workspace - client.workspaces.update(name, \*\*params) -> Workspace - client.workspaces.list(\*\*params) -> SyncDefaultPagination[Workspace] -- client.workspaces.delete(name) -> DeleteResponse +- client.workspaces.delete(name) -> EntityDeleteResponse ## Members @@ -37,4 +38,4 @@ Methods: - client.workspaces.members.create(\*, workspace, \*\*params) -> WorkspaceMember - client.workspaces.members.update(principal_id, \*, workspace, \*\*params) -> WorkspaceMember - client.workspaces.members.list(\*, workspace) -> WorkspaceMemberListResponse -- client.workspaces.members.delete(principal_id, \*, workspace, \*\*params) -> DeleteResponse +- client.workspaces.members.delete(principal_id, \*, workspace, \*\*params) -> EntityDeleteResponse diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/members.py b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/members.py index a604919639..9a48463c59 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/members.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/members.py @@ -31,8 +31,8 @@ ) from ..._base_client import make_request_options from ...types.workspaces import member_create_params, member_delete_params, member_update_params -from ...types.shared.delete_response import DeleteResponse from ...types.workspaces.workspace_member import WorkspaceMember +from ...types.shared.entity_delete_response import EntityDeleteResponse from ...types.workspaces.workspace_member_list_response import WorkspaceMemberListResponse __all__ = ["MembersResource", "AsyncMembersResource"] @@ -249,7 +249,7 @@ def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DeleteResponse: + ) -> EntityDeleteResponse: """ Remove a member from the workspace by revoking all their roles. @@ -296,7 +296,7 @@ def delete( {"wait_role_propagation": wait_role_propagation}, member_delete_params.MemberDeleteParams ), ), - cast_to=DeleteResponse, + cast_to=EntityDeleteResponse, ) @@ -511,7 +511,7 @@ async def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DeleteResponse: + ) -> EntityDeleteResponse: """ Remove a member from the workspace by revoking all their roles. @@ -558,7 +558,7 @@ async def delete( {"wait_role_propagation": wait_role_propagation}, member_delete_params.MemberDeleteParams ), ), - cast_to=DeleteResponse, + cast_to=EntityDeleteResponse, ) diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py index 1144266e61..5ea474ab5b 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py @@ -39,10 +39,15 @@ ) from ...pagination import SyncDefaultPagination, AsyncDefaultPagination from ..._base_client import AsyncPaginator, make_request_options -from ...types.workspaces import workspace_list_params, workspace_create_params, workspace_update_params +from ...types.workspaces import ( + WorkspaceSortField, + workspace_list_params, + workspace_create_params, + workspace_update_params, +) from ...types.workspaces.workspace import Workspace -from ...types.shared.delete_response import DeleteResponse -from ...types.shared.generic_sort_field import GenericSortField +from ...types.shared.entity_delete_response import EntityDeleteResponse +from ...types.workspaces.workspace_sort_field import WorkspaceSortField from ..._exceptions import ConflictError __all__ = ["WorkspacesResource", "AsyncWorkspacesResource"] @@ -237,7 +242,7 @@ def list( filter: str | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: GenericSortField | Omit = omit, + sort: WorkspaceSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -320,7 +325,7 @@ def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DeleteResponse: + ) -> EntityDeleteResponse: """ Delete a workspace. @@ -352,7 +357,7 @@ def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=DeleteResponse, + cast_to=EntityDeleteResponse, ) @@ -547,7 +552,7 @@ def list( filter: str | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: GenericSortField | Omit = omit, + sort: WorkspaceSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -630,7 +635,7 @@ async def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> DeleteResponse: + ) -> EntityDeleteResponse: """ Delete a workspace. @@ -662,7 +667,7 @@ async def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=DeleteResponse, + cast_to=EntityDeleteResponse, ) diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/__init__.py index 0dc61f81ec..904c057a6c 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/__init__.py @@ -43,6 +43,7 @@ PlatformJobLogPage as PlatformJobLogPage, HTTPValidationError as HTTPValidationError, SlidingWindowConfig as SlidingWindowConfig, + EntityDeleteResponse as EntityDeleteResponse, FilesetMetadataParam as FilesetMetadataParam, ModelMetadataContent as ModelMetadataContent, AuthDiscoveryResponse as AuthDiscoveryResponse, diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/__init__.py index 6c686d37f9..eebb5bfff6 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/__init__.py @@ -121,6 +121,7 @@ from .private_ai_detection_param import PrivateAIDetectionParam as PrivateAIDetectionParam from .user_messages_config_param import UserMessagesConfigParam as UserMessagesConfigParam from .content_safety_config_param import ContentSafetyConfigParam as ContentSafetyConfigParam +from .guardrail_config_sort_field import GuardrailConfigSortField as GuardrailConfigSortField from .remote_hf_classifier_config import RemoteHfClassifierConfig as RemoteHfClassifierConfig from .ai_defense_rail_config_param import AIDefenseRailConfigParam as AIDefenseRailConfigParam from .auto_align_rail_config_param import AutoAlignRailConfigParam as AutoAlignRailConfigParam diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/config_list_params.py b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/config_list_params.py index 16e7026394..d0ed816e74 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/config_list_params.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/config_list_params.py @@ -19,7 +19,7 @@ from typing_extensions import TypedDict -from ..shared.generic_sort_field import GenericSortField +from .guardrail_config_sort_field import GuardrailConfigSortField from .guardrail_config_filter_param import GuardrailConfigFilterParam __all__ = ["ConfigListParams"] @@ -40,7 +40,7 @@ class ConfigListParams(TypedDict, total=False): page_size: int """Page size.""" - sort: GenericSortField + sort: GuardrailConfigSortField """The field to sort by. To sort in decreasing order, use `-` in front of the field name. diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/guardrail_config_sort_field.py b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/guardrail_config_sort_field.py new file mode 100644 index 0000000000..8b2eb29619 --- /dev/null +++ b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/guardrail_config_sort_field.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal, TypeAlias + +__all__ = ["GuardrailConfigSortField"] + +GuardrailConfigSortField: TypeAlias = Literal["created_at", "-created_at"] diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/shared/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/shared/__init__.py index e89faaf631..785c6d6519 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/shared/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/shared/__init__.py @@ -40,6 +40,7 @@ from .http_validation_error import HTTPValidationError as HTTPValidationError from .platform_job_log_page import PlatformJobLogPage as PlatformJobLogPage from .sliding_window_config import SlidingWindowConfig as SlidingWindowConfig +from .entity_delete_response import EntityDeleteResponse as EntityDeleteResponse from .fileset_metadata_param import FilesetMetadataParam as FilesetMetadataParam from .model_metadata_content import ModelMetadataContent as ModelMetadataContent from .auth_discovery_response import AuthDiscoveryResponse as AuthDiscoveryResponse diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/shared/entity_delete_response.py b/sdk/python/nemo-platform/src/nemo_platform/types/shared/entity_delete_response.py new file mode 100644 index 0000000000..7c4d604967 --- /dev/null +++ b/sdk/python/nemo-platform/src/nemo_platform/types/shared/entity_delete_response.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["EntityDeleteResponse"] + + +class EntityDeleteResponse(BaseModel): + """Response for successful delete operations.""" + + id: str + """ID of the deleted resource""" + + deleted_count: Optional[int] = None + """Number of items deleted""" + + message: Optional[str] = None diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/__init__.py index 643982631a..70e365427f 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/__init__.py @@ -23,6 +23,7 @@ from .member_create_params import MemberCreateParams as MemberCreateParams from .member_delete_params import MemberDeleteParams as MemberDeleteParams from .member_update_params import MemberUpdateParams as MemberUpdateParams +from .workspace_sort_field import WorkspaceSortField as WorkspaceSortField from .workspace_list_params import WorkspaceListParams as WorkspaceListParams from .workspace_create_params import WorkspaceCreateParams as WorkspaceCreateParams from .workspace_update_params import WorkspaceUpdateParams as WorkspaceUpdateParams diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_list_params.py b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_list_params.py index fbb51a4d6c..07e4cc488e 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_list_params.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_list_params.py @@ -19,7 +19,7 @@ from typing_extensions import TypedDict -from ..shared.generic_sort_field import GenericSortField +from .workspace_sort_field import WorkspaceSortField __all__ = ["WorkspaceListParams"] @@ -43,5 +43,5 @@ class WorkspaceListParams(TypedDict, total=False): page_size: int """Items per page""" - sort: GenericSortField + sort: WorkspaceSortField """Sort field""" diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_sort_field.py b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_sort_field.py new file mode 100644 index 0000000000..bf9e38101d --- /dev/null +++ b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_sort_field.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal, TypeAlias + +__all__ = ["WorkspaceSortField"] + +WorkspaceSortField: TypeAlias = Literal["created_at", "-created_at", "updated_at", "-updated_at"] diff --git a/sdk/python/nemo-platform/tests/api_resources/test_entities.py b/sdk/python/nemo-platform/tests/api_resources/test_entities.py index a76aaf406b..bb0833370d 100644 --- a/sdk/python/nemo-platform/tests/api_resources/test_entities.py +++ b/sdk/python/nemo-platform/tests/api_resources/test_entities.py @@ -25,7 +25,7 @@ from tests.utils import assert_matches_type from nemo_platform import NeMoPlatform, AsyncNeMoPlatform from nemo_platform.pagination import SyncDefaultPagination, AsyncDefaultPagination -from nemo_platform.types.shared import DeleteResponse +from nemo_platform.types.shared import EntityDeleteResponse from nemo_platform.types.entities import ( Entity, ) @@ -179,7 +179,7 @@ def test_method_delete_entity_by_name(self, client: NeMoPlatform) -> None: workspace="workspace", entity_type="entity_type", ) - assert_matches_type(DeleteResponse, entity, path=["response"]) + assert_matches_type(EntityDeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -190,7 +190,7 @@ def test_method_delete_entity_by_name_with_all_params(self, client: NeMoPlatform entity_type="entity_type", parent="parent", ) - assert_matches_type(DeleteResponse, entity, path=["response"]) + assert_matches_type(EntityDeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -204,7 +204,7 @@ def test_raw_response_delete_entity_by_name(self, client: NeMoPlatform) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" entity = response.parse() - assert_matches_type(DeleteResponse, entity, path=["response"]) + assert_matches_type(EntityDeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -218,7 +218,7 @@ def test_streaming_response_delete_entity_by_name(self, client: NeMoPlatform) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" entity = response.parse() - assert_matches_type(DeleteResponse, entity, path=["response"]) + assert_matches_type(EntityDeleteResponse, entity, path=["response"]) assert cast(Any, response.is_closed) is True @@ -597,7 +597,7 @@ async def test_method_delete_entity_by_name(self, async_client: AsyncNeMoPlatfor workspace="workspace", entity_type="entity_type", ) - assert_matches_type(DeleteResponse, entity, path=["response"]) + assert_matches_type(EntityDeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -608,7 +608,7 @@ async def test_method_delete_entity_by_name_with_all_params(self, async_client: entity_type="entity_type", parent="parent", ) - assert_matches_type(DeleteResponse, entity, path=["response"]) + assert_matches_type(EntityDeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -622,7 +622,7 @@ async def test_raw_response_delete_entity_by_name(self, async_client: AsyncNeMoP assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" entity = await response.parse() - assert_matches_type(DeleteResponse, entity, path=["response"]) + assert_matches_type(EntityDeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -636,7 +636,7 @@ async def test_streaming_response_delete_entity_by_name(self, async_client: Asyn assert response.http_request.headers.get("X-Stainless-Lang") == "python" entity = await response.parse() - assert_matches_type(DeleteResponse, entity, path=["response"]) + assert_matches_type(EntityDeleteResponse, entity, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/sdk/python/nemo-platform/tests/api_resources/test_projects.py b/sdk/python/nemo-platform/tests/api_resources/test_projects.py index ce69878ef4..9306ef0b4a 100644 --- a/sdk/python/nemo-platform/tests/api_resources/test_projects.py +++ b/sdk/python/nemo-platform/tests/api_resources/test_projects.py @@ -25,7 +25,7 @@ from tests.utils import assert_matches_type from nemo_platform import NeMoPlatform, AsyncNeMoPlatform from nemo_platform.pagination import SyncDefaultPagination, AsyncDefaultPagination -from nemo_platform.types.shared import DeleteResponse +from nemo_platform.types.shared import EntityDeleteResponse from nemo_platform.types.projects import ( Project, ) @@ -267,7 +267,7 @@ def test_method_delete(self, client: NeMoPlatform) -> None: name="name", workspace="workspace", ) - assert_matches_type(DeleteResponse, project, path=["response"]) + assert_matches_type(EntityDeleteResponse, project, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -280,7 +280,7 @@ def test_raw_response_delete(self, client: NeMoPlatform) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(DeleteResponse, project, path=["response"]) + assert_matches_type(EntityDeleteResponse, project, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -293,7 +293,7 @@ def test_streaming_response_delete(self, client: NeMoPlatform) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(DeleteResponse, project, path=["response"]) + assert_matches_type(EntityDeleteResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -549,7 +549,7 @@ async def test_method_delete(self, async_client: AsyncNeMoPlatform) -> None: name="name", workspace="workspace", ) - assert_matches_type(DeleteResponse, project, path=["response"]) + assert_matches_type(EntityDeleteResponse, project, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -562,7 +562,7 @@ async def test_raw_response_delete(self, async_client: AsyncNeMoPlatform) -> Non assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(DeleteResponse, project, path=["response"]) + assert_matches_type(EntityDeleteResponse, project, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -575,7 +575,7 @@ async def test_streaming_response_delete(self, async_client: AsyncNeMoPlatform) assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(DeleteResponse, project, path=["response"]) + assert_matches_type(EntityDeleteResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/sdk/python/nemo-platform/tests/api_resources/test_workspaces.py b/sdk/python/nemo-platform/tests/api_resources/test_workspaces.py index 03b4497a27..77f0149dd1 100644 --- a/sdk/python/nemo-platform/tests/api_resources/test_workspaces.py +++ b/sdk/python/nemo-platform/tests/api_resources/test_workspaces.py @@ -25,7 +25,7 @@ from tests.utils import assert_matches_type from nemo_platform import NeMoPlatform, AsyncNeMoPlatform from nemo_platform.pagination import SyncDefaultPagination, AsyncDefaultPagination -from nemo_platform.types.shared import DeleteResponse +from nemo_platform.types.shared import EntityDeleteResponse from nemo_platform.types.workspaces import ( Workspace, ) @@ -218,7 +218,7 @@ def test_method_delete(self, client: NeMoPlatform) -> None: workspace = client.workspaces.delete( "name", ) - assert_matches_type(DeleteResponse, workspace, path=["response"]) + assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -230,7 +230,7 @@ def test_raw_response_delete(self, client: NeMoPlatform) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" workspace = response.parse() - assert_matches_type(DeleteResponse, workspace, path=["response"]) + assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -242,7 +242,7 @@ def test_streaming_response_delete(self, client: NeMoPlatform) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" workspace = response.parse() - assert_matches_type(DeleteResponse, workspace, path=["response"]) + assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) assert cast(Any, response.is_closed) is True @@ -442,7 +442,7 @@ async def test_method_delete(self, async_client: AsyncNeMoPlatform) -> None: workspace = await async_client.workspaces.delete( "name", ) - assert_matches_type(DeleteResponse, workspace, path=["response"]) + assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -454,7 +454,7 @@ async def test_raw_response_delete(self, async_client: AsyncNeMoPlatform) -> Non assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" workspace = await response.parse() - assert_matches_type(DeleteResponse, workspace, path=["response"]) + assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -466,7 +466,7 @@ async def test_streaming_response_delete(self, async_client: AsyncNeMoPlatform) assert response.http_request.headers.get("X-Stainless-Lang") == "python" workspace = await response.parse() - assert_matches_type(DeleteResponse, workspace, path=["response"]) + assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/sdk/python/nemo-platform/tests/api_resources/workspaces/test_members.py b/sdk/python/nemo-platform/tests/api_resources/workspaces/test_members.py index f0455784ec..23302f48f4 100644 --- a/sdk/python/nemo-platform/tests/api_resources/workspaces/test_members.py +++ b/sdk/python/nemo-platform/tests/api_resources/workspaces/test_members.py @@ -24,7 +24,7 @@ from tests.utils import assert_matches_type from nemo_platform import NeMoPlatform, AsyncNeMoPlatform -from nemo_platform.types.shared import DeleteResponse +from nemo_platform.types.shared import EntityDeleteResponse from nemo_platform.types.workspaces import ( WorkspaceMember, WorkspaceMemberListResponse, @@ -210,7 +210,7 @@ def test_method_delete(self, client: NeMoPlatform) -> None: principal_id="principal_id", workspace="workspace", ) - assert_matches_type(DeleteResponse, member, path=["response"]) + assert_matches_type(EntityDeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -220,7 +220,7 @@ def test_method_delete_with_all_params(self, client: NeMoPlatform) -> None: workspace="workspace", wait_role_propagation=True, ) - assert_matches_type(DeleteResponse, member, path=["response"]) + assert_matches_type(EntityDeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -233,7 +233,7 @@ def test_raw_response_delete(self, client: NeMoPlatform) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(DeleteResponse, member, path=["response"]) + assert_matches_type(EntityDeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -246,7 +246,7 @@ def test_streaming_response_delete(self, client: NeMoPlatform) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(DeleteResponse, member, path=["response"]) + assert_matches_type(EntityDeleteResponse, member, path=["response"]) assert cast(Any, response.is_closed) is True @@ -445,7 +445,7 @@ async def test_method_delete(self, async_client: AsyncNeMoPlatform) -> None: principal_id="principal_id", workspace="workspace", ) - assert_matches_type(DeleteResponse, member, path=["response"]) + assert_matches_type(EntityDeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -455,7 +455,7 @@ async def test_method_delete_with_all_params(self, async_client: AsyncNeMoPlatfo workspace="workspace", wait_role_propagation=True, ) - assert_matches_type(DeleteResponse, member, path=["response"]) + assert_matches_type(EntityDeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -468,7 +468,7 @@ async def test_raw_response_delete(self, async_client: AsyncNeMoPlatform) -> Non assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(DeleteResponse, member, path=["response"]) + assert_matches_type(EntityDeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -481,7 +481,7 @@ async def test_streaming_response_delete(self, async_client: AsyncNeMoPlatform) assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(DeleteResponse, member, path=["response"]) + assert_matches_type(EntityDeleteResponse, member, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/sdk/stainless.yaml b/sdk/stainless.yaml index 76037a1354..08432deefa 100644 --- a/sdk/stainless.yaml +++ b/sdk/stainless.yaml @@ -368,6 +368,7 @@ resources: models: guardrail_config_filter: GuardrailConfigFilter guardrail_config_param: GuardrailConfigInput + guardrail_config_sort_field: GuardrailConfigSortField guardrail_config_update: GuardrailConfigUpdate guardrail_configs_page: GuardrailConfigsPage methods: @@ -648,6 +649,7 @@ resources: models: workspace: Workspace workspace_param: WorkspaceInput + workspace_sort_field: WorkspaceSortField workspace_update: WorkspaceUpdate workspaces_page: WorkspacesPage methods: @@ -751,6 +753,7 @@ resources: workload_token_exchange_error_response: WorkloadTokenExchangeErrorResponse json_web_key: JsonWebKey json_web_key_set_response: JsonWebKeySetResponse + entity_delete_response: EntityDeleteResponse iam: standalone_api: true subresources: From d80e3dad45f2f1f0d182af95ef5f1ef2e2bfed53 Mon Sep 17 00:00:00 2001 From: Albert Cui Date: Mon, 20 Jul 2026 12:39:40 -0400 Subject: [PATCH 4/9] fix lint Signed-off-by: Albert Cui --- docs/cli/reference.mdx | 4 ++-- .../nemo_platform_ext/cli/commands/api/guardrail/configs.py | 2 +- .../nemo_platform_ext/cli/commands/api/workspaces/__init__.py | 3 ++- .../src/nemo_platform/cli/commands/api/guardrail/configs.py | 2 +- .../src/nemo_platform/cli/commands/api/workspaces/__init__.py | 3 ++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index 0db8e879b8..400717820a 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -5132,7 +5132,7 @@ nemo workspaces list [OPTIONS] - Object (JSON): `{"name":{"$like":"value"}}` with operators `$eq`, `$like`, `$lt`, `$lte`, `$gt`, `$gte`, `$in`, `$nin`, `$and`, `$or`, `$not` * `--page `: Page number * `--page-size `: Items per page -* `--sort `: Sort field [possible values: created_at, -created_at, name, -name] +* `--sort `: Sort field [possible values: created_at, -created_at, updated_at, -updated_at] * `--all-pages`: Fetch all pages **Help:** @@ -5609,7 +5609,7 @@ nemo guardrail configs list [OPTIONS] * `--workspace` * `--page `: Page number. * `--page-size `: Page size. -* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: created_at, -created_at, name, -name] +* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: created_at, -created_at] * `--all-pages`: Fetch all pages **Filter Options:** diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/guardrail/configs.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/guardrail/configs.py index 606c20f645..f49d96a78a 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/guardrail/configs.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/guardrail/configs.py @@ -152,7 +152,7 @@ def list_configs( page: Annotated[int | None, typer.Option("--page", help="Page number.")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Page size.")] = None, sort: Annotated[ - Literal["created_at", "-created_at", "name", "-name"] | None, + Literal["created_at", "-created_at"] | None, typer.Option( "--sort", help="The field to sort by. To sort in decreasing order, use `-` in front of the field name." ), diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/workspaces/__init__.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/workspaces/__init__.py index eb13d83006..2df9de0763 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/workspaces/__init__.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/workspaces/__init__.py @@ -179,7 +179,8 @@ def list_workspaces( page: Annotated[int | None, typer.Option("--page", help="Page number")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Items per page")] = None, sort: Annotated[ - Literal["created_at", "-created_at", "name", "-name"] | None, typer.Option("--sort", help="Sort field") + Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None, + typer.Option("--sort", help="Sort field"), ] = None, output_format: ListOutputFormatOption = None, no_truncate: NoTruncateOption = None, diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/guardrail/configs.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/guardrail/configs.py index 230fca30f9..43e9a89058 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/guardrail/configs.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/guardrail/configs.py @@ -152,7 +152,7 @@ def list_configs( page: Annotated[int | None, typer.Option("--page", help="Page number.")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Page size.")] = None, sort: Annotated[ - Literal["created_at", "-created_at", "name", "-name"] | None, + Literal["created_at", "-created_at"] | None, typer.Option( "--sort", help="The field to sort by. To sort in decreasing order, use `-` in front of the field name." ), diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/workspaces/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/workspaces/__init__.py index 31655558d6..d7584d2218 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/workspaces/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/workspaces/__init__.py @@ -179,7 +179,8 @@ def list_workspaces( page: Annotated[int | None, typer.Option("--page", help="Page number")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Items per page")] = None, sort: Annotated[ - Literal["created_at", "-created_at", "name", "-name"] | None, typer.Option("--sort", help="Sort field") + Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None, + typer.Option("--sort", help="Sort field"), ] = None, output_format: ListOutputFormatOption = None, no_truncate: NoTruncateOption = None, From d5558b6d97d8fb8f3fcd32e3016d554334c4045b Mon Sep 17 00:00:00 2001 From: Albert Cui Date: Mon, 20 Jul 2026 15:24:13 -0400 Subject: [PATCH 5/9] fix(guardrails): share GenericSortField and support updated_at sorting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guardrails' config list defined its own GuardrailConfigSortField (created_at only) in the legacy nmp.guardrails.app.common.common. GuardrailConfig is entity-store-backed, so it can sort by any entity base field; point it at the shared nmp.common.api.common.GenericSortField and retire the local enum. Widen GenericSortField with updated_at — both guardrail configs and filesets are entity-backed and the store already supports the column — and convert it from (str, Enum) to StrEnum. A bare (str, Enum) member stringifies to "GenericSortField.CREATED_AT_ASC" rather than "created_at", so passing the member straight to the entity client produced a malformed sort and broke the list request; StrEnum makes members stringify to their value. Guardrails now passes sort.value, matching the filesets endpoint. Regenerate the OpenAPI spec and Python SDK for the removed GuardrailConfigSortField and widened GenericSortField. Signed-off-by: Albert Cui --- openapi/ga/individual/platform.openapi.yaml | 14 ++++++------ openapi/ga/openapi.yaml | 14 ++++++------ openapi/openapi.yaml | 14 ++++++------ .../nmp_common/src/nmp/common/api/common.py | 14 ++++++++++-- .../nemo-platform/.nmpcontext/openapi.yaml | 14 ++++++------ .../nemo-platform/.nmpcontext/stainless.yaml | 1 - .../nemo_platform/resources/guardrail/api.md | 1 - .../resources/guardrail/configs.py | 13 ++++------- .../nemo_platform/types/guardrail/__init__.py | 1 - .../types/guardrail/config_list_params.py | 4 ++-- .../guardrail/guardrail_config_sort_field.py | 22 ------------------- .../types/shared/generic_sort_field.py | 2 +- .../types/shared_params/generic_sort_field.py | 2 +- sdk/stainless.yaml | 1 - .../guardrails/api/v2/configs/endpoints.py | 7 +++--- .../src/nmp/guardrails/app/common/common.py | 7 +----- 16 files changed, 52 insertions(+), 79 deletions(-) delete mode 100644 sdk/python/nemo-platform/src/nemo_platform/types/guardrail/guardrail_config_sort_field.py diff --git a/openapi/ga/individual/platform.openapi.yaml b/openapi/ga/individual/platform.openapi.yaml index 2a04ecbe75..824d100329 100644 --- a/openapi/ga/individual/platform.openapi.yaml +++ b/openapi/ga/individual/platform.openapi.yaml @@ -2002,7 +2002,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GuardrailConfigSortField' + - $ref: '#/components/schemas/GenericSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: created_at @@ -11999,9 +11999,15 @@ components: enum: - created_at - -created_at + - updated_at + - -updated_at - name - -name title: GenericSortField + description: "Sort options for entity-store-backed list endpoints.\n\nMembers\ + \ map to entity base columns (see ``BASE_FIELDS``), so any\nentity-backed\ + \ resource (filesets, guardrail configs, \u2026) can share this\nwithout advertising\ + \ a sort it cannot honor. Prefix a field with ``-`` for\ndescending order." GuardrailCheckRequest: properties: model: @@ -12320,12 +12326,6 @@ components: - name title: GuardrailConfigInput description: Input schema for creating a guardrail config. - GuardrailConfigSortField: - type: string - enum: - - created_at - - -created_at - title: GuardrailConfigSortField GuardrailConfigUpdate: properties: description: diff --git a/openapi/ga/openapi.yaml b/openapi/ga/openapi.yaml index 2a04ecbe75..824d100329 100644 --- a/openapi/ga/openapi.yaml +++ b/openapi/ga/openapi.yaml @@ -2002,7 +2002,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GuardrailConfigSortField' + - $ref: '#/components/schemas/GenericSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: created_at @@ -11999,9 +11999,15 @@ components: enum: - created_at - -created_at + - updated_at + - -updated_at - name - -name title: GenericSortField + description: "Sort options for entity-store-backed list endpoints.\n\nMembers\ + \ map to entity base columns (see ``BASE_FIELDS``), so any\nentity-backed\ + \ resource (filesets, guardrail configs, \u2026) can share this\nwithout advertising\ + \ a sort it cannot honor. Prefix a field with ``-`` for\ndescending order." GuardrailCheckRequest: properties: model: @@ -12320,12 +12326,6 @@ components: - name title: GuardrailConfigInput description: Input schema for creating a guardrail config. - GuardrailConfigSortField: - type: string - enum: - - created_at - - -created_at - title: GuardrailConfigSortField GuardrailConfigUpdate: properties: description: diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 2a04ecbe75..824d100329 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -2002,7 +2002,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GuardrailConfigSortField' + - $ref: '#/components/schemas/GenericSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: created_at @@ -11999,9 +11999,15 @@ components: enum: - created_at - -created_at + - updated_at + - -updated_at - name - -name title: GenericSortField + description: "Sort options for entity-store-backed list endpoints.\n\nMembers\ + \ map to entity base columns (see ``BASE_FIELDS``), so any\nentity-backed\ + \ resource (filesets, guardrail configs, \u2026) can share this\nwithout advertising\ + \ a sort it cannot honor. Prefix a field with ``-`` for\ndescending order." GuardrailCheckRequest: properties: model: @@ -12320,12 +12326,6 @@ components: - name title: GuardrailConfigInput description: Input schema for creating a guardrail config. - GuardrailConfigSortField: - type: string - enum: - - created_at - - -created_at - title: GuardrailConfigSortField GuardrailConfigUpdate: properties: description: diff --git a/packages/nmp_common/src/nmp/common/api/common.py b/packages/nmp_common/src/nmp/common/api/common.py index bb7777fb25..1d38247454 100644 --- a/packages/nmp_common/src/nmp/common/api/common.py +++ b/packages/nmp_common/src/nmp/common/api/common.py @@ -4,7 +4,7 @@ """Common structures used across multiple API endpoints/schemas.""" from datetime import datetime, timezone -from enum import Enum +from enum import Enum, StrEnum from typing import Generic, List, Optional, TypeVar from nemo_platform_plugin.schema import Page as Page @@ -58,9 +58,19 @@ class PaginatedResult(BaseModel, Generic[T]): pagination: PaginationData -class GenericSortField(str, Enum): +class GenericSortField(StrEnum): + """Sort options for entity-store-backed list endpoints. + + Members map to entity base columns (see ``BASE_FIELDS``), so any + entity-backed resource (filesets, guardrail configs, …) can share this + without advertising a sort it cannot honor. Prefix a field with ``-`` for + descending order. + """ + CREATED_AT_ASC = "created_at" CREATED_AT_DESC = "-created_at" + UPDATED_AT_ASC = "updated_at" + UPDATED_AT_DESC = "-updated_at" NAME_ASC = "name" NAME_DESC = "-name" diff --git a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml index 2a04ecbe75..824d100329 100644 --- a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml @@ -2002,7 +2002,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/GuardrailConfigSortField' + - $ref: '#/components/schemas/GenericSortField' description: The field to sort by. To sort in decreasing order, use `-` in front of the field name. default: created_at @@ -11999,9 +11999,15 @@ components: enum: - created_at - -created_at + - updated_at + - -updated_at - name - -name title: GenericSortField + description: "Sort options for entity-store-backed list endpoints.\n\nMembers\ + \ map to entity base columns (see ``BASE_FIELDS``), so any\nentity-backed\ + \ resource (filesets, guardrail configs, \u2026) can share this\nwithout advertising\ + \ a sort it cannot honor. Prefix a field with ``-`` for\ndescending order." GuardrailCheckRequest: properties: model: @@ -12320,12 +12326,6 @@ components: - name title: GuardrailConfigInput description: Input schema for creating a guardrail config. - GuardrailConfigSortField: - type: string - enum: - - created_at - - -created_at - title: GuardrailConfigSortField GuardrailConfigUpdate: properties: description: diff --git a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml index 08432deefa..ee0a16bab8 100644 --- a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml @@ -368,7 +368,6 @@ resources: models: guardrail_config_filter: GuardrailConfigFilter guardrail_config_param: GuardrailConfigInput - guardrail_config_sort_field: GuardrailConfigSortField guardrail_config_update: GuardrailConfigUpdate guardrail_configs_page: GuardrailConfigsPage methods: diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md index c20d87c7cd..29e61b6bd4 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/api.md @@ -107,7 +107,6 @@ Types: from nemo_platform.types.guardrail import ( GuardrailConfigFilter, GuardrailConfigParam, - GuardrailConfigSortField, GuardrailConfigUpdate, GuardrailConfigsPage, ) diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/configs.py b/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/configs.py index d3715c9b81..28b444f882 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/configs.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/guardrail/configs.py @@ -33,15 +33,10 @@ ) from ...pagination import SyncDefaultPagination, AsyncDefaultPagination from ..._base_client import AsyncPaginator, make_request_options -from ...types.guardrail import ( - GuardrailConfigSortField, - config_list_params, - config_create_params, - config_update_params, -) +from ...types.guardrail import config_list_params, config_create_params, config_update_params from ...types.shared.delete_response import DeleteResponse +from ...types.shared.generic_sort_field import GenericSortField from ...types.guardrail.guardrail_config import GuardrailConfig -from ...types.guardrail.guardrail_config_sort_field import GuardrailConfigSortField from ...types.guardrail.guardrail_config_filter_param import GuardrailConfigFilterParam from ..._exceptions import ConflictError @@ -228,7 +223,7 @@ def list( filter: GuardrailConfigFilterParam | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: GuardrailConfigSortField | Omit = omit, + sort: GenericSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -504,7 +499,7 @@ def list( filter: GuardrailConfigFilterParam | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: GuardrailConfigSortField | Omit = omit, + sort: GenericSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/__init__.py index eebb5bfff6..6c686d37f9 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/__init__.py @@ -121,7 +121,6 @@ from .private_ai_detection_param import PrivateAIDetectionParam as PrivateAIDetectionParam from .user_messages_config_param import UserMessagesConfigParam as UserMessagesConfigParam from .content_safety_config_param import ContentSafetyConfigParam as ContentSafetyConfigParam -from .guardrail_config_sort_field import GuardrailConfigSortField as GuardrailConfigSortField from .remote_hf_classifier_config import RemoteHfClassifierConfig as RemoteHfClassifierConfig from .ai_defense_rail_config_param import AIDefenseRailConfigParam as AIDefenseRailConfigParam from .auto_align_rail_config_param import AutoAlignRailConfigParam as AutoAlignRailConfigParam diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/config_list_params.py b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/config_list_params.py index d0ed816e74..16e7026394 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/config_list_params.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/config_list_params.py @@ -19,7 +19,7 @@ from typing_extensions import TypedDict -from .guardrail_config_sort_field import GuardrailConfigSortField +from ..shared.generic_sort_field import GenericSortField from .guardrail_config_filter_param import GuardrailConfigFilterParam __all__ = ["ConfigListParams"] @@ -40,7 +40,7 @@ class ConfigListParams(TypedDict, total=False): page_size: int """Page size.""" - sort: GuardrailConfigSortField + sort: GenericSortField """The field to sort by. To sort in decreasing order, use `-` in front of the field name. diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/guardrail_config_sort_field.py b/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/guardrail_config_sort_field.py deleted file mode 100644 index 8b2eb29619..0000000000 --- a/sdk/python/nemo-platform/src/nemo_platform/types/guardrail/guardrail_config_sort_field.py +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["GuardrailConfigSortField"] - -GuardrailConfigSortField: TypeAlias = Literal["created_at", "-created_at"] diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/shared/generic_sort_field.py b/sdk/python/nemo-platform/src/nemo_platform/types/shared/generic_sort_field.py index baed1025a0..f9b995563b 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/shared/generic_sort_field.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/shared/generic_sort_field.py @@ -19,4 +19,4 @@ __all__ = ["GenericSortField"] -GenericSortField: TypeAlias = Literal["created_at", "-created_at", "name", "-name"] +GenericSortField: TypeAlias = Literal["created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"] diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/shared_params/generic_sort_field.py b/sdk/python/nemo-platform/src/nemo_platform/types/shared_params/generic_sort_field.py index 18e079f738..e44138ea03 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/shared_params/generic_sort_field.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/shared_params/generic_sort_field.py @@ -21,4 +21,4 @@ __all__ = ["GenericSortField"] -GenericSortField: TypeAlias = Literal["created_at", "-created_at", "name", "-name"] +GenericSortField: TypeAlias = Literal["created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"] diff --git a/sdk/stainless.yaml b/sdk/stainless.yaml index 08432deefa..ee0a16bab8 100644 --- a/sdk/stainless.yaml +++ b/sdk/stainless.yaml @@ -368,7 +368,6 @@ resources: models: guardrail_config_filter: GuardrailConfigFilter guardrail_config_param: GuardrailConfigInput - guardrail_config_sort_field: GuardrailConfigSortField guardrail_config_update: GuardrailConfigUpdate guardrail_configs_page: GuardrailConfigsPage methods: diff --git a/services/guardrails/src/nmp/guardrails/api/v2/configs/endpoints.py b/services/guardrails/src/nmp/guardrails/api/v2/configs/endpoints.py index 0074979387..53ccc44dc7 100644 --- a/services/guardrails/src/nmp/guardrails/api/v2/configs/endpoints.py +++ b/services/guardrails/src/nmp/guardrails/api/v2/configs/endpoints.py @@ -7,13 +7,12 @@ from fastapi import APIRouter, Depends, HTTPException, Query from nmp.common.api import ParsedFilter, make_filter_dep -from nmp.common.api.common import DeleteResponse, Page, PaginationData +from nmp.common.api.common import DeleteResponse, GenericSortField, Page, PaginationData from nmp.common.api.utils import generate_openapi_extra_params from nmp.common.entities import EntityClient, EntityConflictError, EntityNotFoundError from nmp.common.service.dependencies import get_entity_client from nmp.guardrails.api.dependencies import ConfigRegistryDep, RailsRegistryDep from nmp.guardrails.api.v2.configs.schemas import GuardrailConfigFilter, GuardrailConfigInput, GuardrailConfigUpdate -from nmp.guardrails.app.common.common import GuardrailConfigSortField from nmp.guardrails.app.utils.config_utils import enrich_config_with_data, invalidate_and_reload_config_cache from nmp.guardrails.entities import GuardrailConfig @@ -36,7 +35,7 @@ async def list_guardrail_configs( entities_client: EntityClient = Depends(get_entity_client), page: int = Query(default=1, description="Page number."), page_size: int = Query(default=10, description="Page size."), - sort: GuardrailConfigSortField = Query( + sort: GenericSortField = Query( default="created_at", description="""The field to sort by. To sort in decreasing order, use `-` in front of the field name.""", ), @@ -50,7 +49,7 @@ async def list_guardrail_configs( GuardrailConfig, page=page, page_size=page_size, - sort=sort, + sort=sort.value, workspace=workspace, filter_operation=parsed.operation, ) diff --git a/services/guardrails/src/nmp/guardrails/app/common/common.py b/services/guardrails/src/nmp/guardrails/app/common/common.py index 4e8e620d34..0885138927 100644 --- a/services/guardrails/src/nmp/guardrails/app/common/common.py +++ b/services/guardrails/src/nmp/guardrails/app/common/common.py @@ -3,7 +3,7 @@ """Common structures used across multiple API endpoints/schemas.""" -from enum import Enum, StrEnum +from enum import Enum from nmp.common.entities.values import Value from pydantic import BaseModel, Field @@ -20,11 +20,6 @@ class URN(str): """ -class GuardrailConfigSortField(StrEnum): - CREATED_AT_ASC = "created_at" - CREATED_AT_DESC = "-created_at" - - class ErrorResponse(Value): detail: str = Field(json_schema_extra={"example": "Error message"}) From 4725f346ce119ca9712e2f12947a79e379b2314c Mon Sep 17 00:00:00 2001 From: Albert Cui Date: Mon, 20 Jul 2026 15:55:56 -0400 Subject: [PATCH 6/9] fix lint Signed-off-by: Albert Cui --- docs/cli/reference.mdx | 8 ++++---- .../nemo_platform_ext/cli/commands/api/files/filesets.py | 2 +- .../cli/commands/api/guardrail/configs.py | 2 +- .../cli/commands/api/inference/prompts.py | 2 +- .../src/nemo_platform/cli/commands/api/files/filesets.py | 2 +- .../nemo_platform/cli/commands/api/guardrail/configs.py | 2 +- .../nemo_platform/cli/commands/api/inference/prompts.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index 400717820a..f5d3c55e13 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -1160,7 +1160,7 @@ nemo files filesets list [OPTIONS] * `--workspace` * `--page `: Page number. * `--page-size `: Page size. -* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: created_at, -created_at, name, -name] +* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: created_at, -created_at, updated_at, -updated_at, name, -name] * `--all-pages`: Fetch all pages **Filter Options:** @@ -2980,7 +2980,7 @@ nemo inference prompts list [OPTIONS] * `--workspace` * `--page `: Page number. * `--page-size `: Page size. -* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: name, -name, created_at, -created_at, updated_at, -updated_at] +* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: created_at, -created_at, updated_at, -updated_at, name, -name] * `--all-pages`: Fetch all pages **Filter Options:** @@ -4483,7 +4483,7 @@ nemo models list [OPTIONS] * `--workspace` * `--page `: Page number. * `--page-size `: Page size. -* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: name, -name, created_at, -created_at, updated_at, -updated_at] +* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: created_at, -created_at, updated_at, -updated_at, name, -name] * `--verbose`: Whether to include full spec details * `--all-pages`: Fetch all pages @@ -5609,7 +5609,7 @@ nemo guardrail configs list [OPTIONS] * `--workspace` * `--page `: Page number. * `--page-size `: Page size. -* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: created_at, -created_at] +* `--sort `: The field to sort by. To sort in decreasing order, use `-` in front of the field name. [possible values: created_at, -created_at, updated_at, -updated_at, name, -name] * `--all-pages`: Fetch all pages **Filter Options:** diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/files/filesets.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/files/filesets.py index 4ab9ca46ba..9ea72faeeb 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/files/filesets.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/files/filesets.py @@ -202,7 +202,7 @@ def list_filesets( page: Annotated[int | None, typer.Option("--page", help="Page number.")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Page size.")] = None, sort: Annotated[ - Literal["created_at", "-created_at", "name", "-name"] | None, + Literal["created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"] | None, typer.Option( "--sort", help="The field to sort by. To sort in decreasing order, use `-` in front of the field name." ), diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/guardrail/configs.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/guardrail/configs.py index f49d96a78a..9bc6a4068f 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/guardrail/configs.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/guardrail/configs.py @@ -152,7 +152,7 @@ def list_configs( page: Annotated[int | None, typer.Option("--page", help="Page number.")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Page size.")] = None, sort: Annotated[ - Literal["created_at", "-created_at"] | None, + Literal["created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"] | None, typer.Option( "--sort", help="The field to sort by. To sort in decreasing order, use `-` in front of the field name." ), diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/prompts.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/prompts.py index 27a4a9daae..523a88a1c6 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/prompts.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/inference/prompts.py @@ -184,7 +184,7 @@ def list_prompts( page: Annotated[int | None, typer.Option("--page", help="Page number.")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Page size.")] = None, sort: Annotated[ - Literal["name", "-name", "created_at", "-created_at", "updated_at", "-updated_at"] | None, + Literal["created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"] | None, typer.Option( "--sort", help="The field to sort by. To sort in decreasing order, use `-` in front of the field name." ), diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/files/filesets.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/files/filesets.py index b07046d678..b00a5618bd 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/files/filesets.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/files/filesets.py @@ -202,7 +202,7 @@ def list_filesets( page: Annotated[int | None, typer.Option("--page", help="Page number.")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Page size.")] = None, sort: Annotated[ - Literal["created_at", "-created_at", "name", "-name"] | None, + Literal["created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"] | None, typer.Option( "--sort", help="The field to sort by. To sort in decreasing order, use `-` in front of the field name." ), diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/guardrail/configs.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/guardrail/configs.py index 43e9a89058..905b2d26e4 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/guardrail/configs.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/guardrail/configs.py @@ -152,7 +152,7 @@ def list_configs( page: Annotated[int | None, typer.Option("--page", help="Page number.")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Page size.")] = None, sort: Annotated[ - Literal["created_at", "-created_at"] | None, + Literal["created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"] | None, typer.Option( "--sort", help="The field to sort by. To sort in decreasing order, use `-` in front of the field name." ), diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/prompts.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/prompts.py index 3b0324df87..287ee600eb 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/prompts.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/inference/prompts.py @@ -184,7 +184,7 @@ def list_prompts( page: Annotated[int | None, typer.Option("--page", help="Page number.")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Page size.")] = None, sort: Annotated[ - Literal["name", "-name", "created_at", "-created_at", "updated_at", "-updated_at"] | None, + Literal["created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"] | None, typer.Option( "--sort", help="The field to sort by. To sort in decreasing order, use `-` in front of the field name." ), From e9c288d6ebfe0111f32e584457a3edd1d2ce8574 Mon Sep 17 00:00:00 2001 From: Albert Cui Date: Mon, 20 Jul 2026 16:18:32 -0400 Subject: [PATCH 7/9] refactor(entities): share GenericSortField for workspace/project sort Workspaces and projects each defined their own sort enum (WorkspaceSortField, ProjectSortField) duplicating the shared nmp.common.api.common.GenericSortField. Both are entity-store-backed and sort via getattr over real base columns, so point them at the shared enum and delete the bespoke ones. ProjectSortField was already identical (created_at, updated_at, name); workspaces additively gains name-sorting (DBWorkspace.name is a real, unique column). This also retires ProjectSortField's stale (str, Enum) base in favor of the shared StrEnum. Regenerate the OpenAPI spec, Python SDK, and CLI for the removed schemas and the widened workspace sort options. Signed-off-by: Albert Cui --- docs/cli/reference.mdx | 2 +- openapi/ga/individual/platform.openapi.yaml | 39 +++++++------------ openapi/ga/openapi.yaml | 39 +++++++------------ openapi/openapi.yaml | 39 +++++++------------ .../cli/commands/api/workspaces/__init__.py | 2 +- .../nmp_common/src/nmp/common/api/common.py | 4 +- .../nemo-platform/.nmpcontext/openapi.yaml | 39 +++++++------------ .../nemo-platform/.nmpcontext/stainless.yaml | 2 - .../cli/commands/api/workspaces/__init__.py | 2 +- .../nemo_platform/resources/projects/api.md | 8 +--- .../resources/projects/projects.py | 8 ++-- .../nemo_platform/resources/workspaces/api.md | 1 - .../resources/workspaces/workspaces.py | 13 ++----- .../nemo_platform/types/projects/__init__.py | 1 - .../types/projects/project_list_params.py | 4 +- .../types/projects/project_sort_field.py | 22 ----------- .../types/workspaces/__init__.py | 1 - .../types/workspaces/workspace_list_params.py | 4 +- .../types/workspaces/workspace_sort_field.py | 22 ----------- .../tests/api_resources/test_projects.py | 4 +- sdk/stainless.yaml | 2 - .../entities/api/v2/projects/endpoints.py | 5 +-- .../core/entities/api/v2/projects/schemas.py | 12 ------ .../src/nmp/core/entities/api/v2/schemas.py | 11 ------ .../entities/api/v2/workspaces/endpoints.py | 6 +-- 25 files changed, 76 insertions(+), 216 deletions(-) delete mode 100644 sdk/python/nemo-platform/src/nemo_platform/types/projects/project_sort_field.py delete mode 100644 sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_sort_field.py diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index f5d3c55e13..caae36c381 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -5132,7 +5132,7 @@ nemo workspaces list [OPTIONS] - Object (JSON): `{"name":{"$like":"value"}}` with operators `$eq`, `$like`, `$lt`, `$lte`, `$gt`, `$gte`, `$in`, `$nin`, `$and`, `$or`, `$not` * `--page `: Page number * `--page-size `: Items per page -* `--sort `: Sort field [possible values: created_at, -created_at, updated_at, -updated_at] +* `--sort `: Sort field [possible values: created_at, -created_at, updated_at, -updated_at, name, -name] * `--all-pages`: Fetch all pages **Help:** diff --git a/openapi/ga/individual/platform.openapi.yaml b/openapi/ga/individual/platform.openapi.yaml index 824d100329..1a4d8888c2 100644 --- a/openapi/ga/individual/platform.openapi.yaml +++ b/openapi/ga/individual/platform.openapi.yaml @@ -414,7 +414,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/WorkspaceSortField' + - $ref: '#/components/schemas/GenericSortField' description: Sort field default: -created_at description: Sort field @@ -1199,7 +1199,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/ProjectSortField' + - $ref: '#/components/schemas/GenericSortField' description: Sort field default: -created_at description: Sort field @@ -12004,10 +12004,17 @@ components: - name - -name title: GenericSortField - description: "Sort options for entity-store-backed list endpoints.\n\nMembers\ - \ map to entity base columns (see ``BASE_FIELDS``), so any\nentity-backed\ - \ resource (filesets, guardrail configs, \u2026) can share this\nwithout advertising\ - \ a sort it cannot honor. Prefix a field with ``-`` for\ndescending order." + description: 'Sort options for entity-store-backed list endpoints. + + + Members map to entity base columns (see ``BASE_FIELDS``), so any + + entity-backed resource (filesets, guardrail configs, workspaces, projects) + + can share this without advertising a sort it cannot honor. Prefix a field + with ``-`` for + + descending order.' GuardrailCheckRequest: properties: model: @@ -16516,17 +16523,6 @@ components: - name title: ProjectInput description: Schema for creating a new project. - ProjectSortField: - type: string - enum: - - created_at - - -created_at - - updated_at - - -updated_at - - name - - -name - title: ProjectSortField - description: Fields available for sorting project results. ProjectUpdate: properties: description: @@ -19652,15 +19648,6 @@ components: - roles title: WorkspaceMemberUpdate description: Schema for updating a workspace member's roles. - WorkspaceSortField: - type: string - enum: - - created_at - - -created_at - - updated_at - - -updated_at - title: WorkspaceSortField - description: Fields available for sorting workspace results. WorkspaceUpdate: properties: description: diff --git a/openapi/ga/openapi.yaml b/openapi/ga/openapi.yaml index 824d100329..1a4d8888c2 100644 --- a/openapi/ga/openapi.yaml +++ b/openapi/ga/openapi.yaml @@ -414,7 +414,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/WorkspaceSortField' + - $ref: '#/components/schemas/GenericSortField' description: Sort field default: -created_at description: Sort field @@ -1199,7 +1199,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/ProjectSortField' + - $ref: '#/components/schemas/GenericSortField' description: Sort field default: -created_at description: Sort field @@ -12004,10 +12004,17 @@ components: - name - -name title: GenericSortField - description: "Sort options for entity-store-backed list endpoints.\n\nMembers\ - \ map to entity base columns (see ``BASE_FIELDS``), so any\nentity-backed\ - \ resource (filesets, guardrail configs, \u2026) can share this\nwithout advertising\ - \ a sort it cannot honor. Prefix a field with ``-`` for\ndescending order." + description: 'Sort options for entity-store-backed list endpoints. + + + Members map to entity base columns (see ``BASE_FIELDS``), so any + + entity-backed resource (filesets, guardrail configs, workspaces, projects) + + can share this without advertising a sort it cannot honor. Prefix a field + with ``-`` for + + descending order.' GuardrailCheckRequest: properties: model: @@ -16516,17 +16523,6 @@ components: - name title: ProjectInput description: Schema for creating a new project. - ProjectSortField: - type: string - enum: - - created_at - - -created_at - - updated_at - - -updated_at - - name - - -name - title: ProjectSortField - description: Fields available for sorting project results. ProjectUpdate: properties: description: @@ -19652,15 +19648,6 @@ components: - roles title: WorkspaceMemberUpdate description: Schema for updating a workspace member's roles. - WorkspaceSortField: - type: string - enum: - - created_at - - -created_at - - updated_at - - -updated_at - title: WorkspaceSortField - description: Fields available for sorting workspace results. WorkspaceUpdate: properties: description: diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 824d100329..1a4d8888c2 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -414,7 +414,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/WorkspaceSortField' + - $ref: '#/components/schemas/GenericSortField' description: Sort field default: -created_at description: Sort field @@ -1199,7 +1199,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/ProjectSortField' + - $ref: '#/components/schemas/GenericSortField' description: Sort field default: -created_at description: Sort field @@ -12004,10 +12004,17 @@ components: - name - -name title: GenericSortField - description: "Sort options for entity-store-backed list endpoints.\n\nMembers\ - \ map to entity base columns (see ``BASE_FIELDS``), so any\nentity-backed\ - \ resource (filesets, guardrail configs, \u2026) can share this\nwithout advertising\ - \ a sort it cannot honor. Prefix a field with ``-`` for\ndescending order." + description: 'Sort options for entity-store-backed list endpoints. + + + Members map to entity base columns (see ``BASE_FIELDS``), so any + + entity-backed resource (filesets, guardrail configs, workspaces, projects) + + can share this without advertising a sort it cannot honor. Prefix a field + with ``-`` for + + descending order.' GuardrailCheckRequest: properties: model: @@ -16516,17 +16523,6 @@ components: - name title: ProjectInput description: Schema for creating a new project. - ProjectSortField: - type: string - enum: - - created_at - - -created_at - - updated_at - - -updated_at - - name - - -name - title: ProjectSortField - description: Fields available for sorting project results. ProjectUpdate: properties: description: @@ -19652,15 +19648,6 @@ components: - roles title: WorkspaceMemberUpdate description: Schema for updating a workspace member's roles. - WorkspaceSortField: - type: string - enum: - - created_at - - -created_at - - updated_at - - -updated_at - title: WorkspaceSortField - description: Fields available for sorting workspace results. WorkspaceUpdate: properties: description: diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/workspaces/__init__.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/workspaces/__init__.py index 2df9de0763..e0a5be7791 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/workspaces/__init__.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/workspaces/__init__.py @@ -179,7 +179,7 @@ def list_workspaces( page: Annotated[int | None, typer.Option("--page", help="Page number")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Items per page")] = None, sort: Annotated[ - Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None, + Literal["name", "-name", "created_at", "-created_at", "updated_at", "-updated_at"] | None, typer.Option("--sort", help="Sort field"), ] = None, output_format: ListOutputFormatOption = None, diff --git a/packages/nmp_common/src/nmp/common/api/common.py b/packages/nmp_common/src/nmp/common/api/common.py index 1d38247454..9b86244407 100644 --- a/packages/nmp_common/src/nmp/common/api/common.py +++ b/packages/nmp_common/src/nmp/common/api/common.py @@ -62,8 +62,8 @@ class GenericSortField(StrEnum): """Sort options for entity-store-backed list endpoints. Members map to entity base columns (see ``BASE_FIELDS``), so any - entity-backed resource (filesets, guardrail configs, …) can share this - without advertising a sort it cannot honor. Prefix a field with ``-`` for + entity-backed resource (filesets, guardrail configs, workspaces, projects) + can share this without advertising a sort it cannot honor. Prefix a field with ``-`` for descending order. """ diff --git a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml index 824d100329..1a4d8888c2 100644 --- a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml @@ -414,7 +414,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/WorkspaceSortField' + - $ref: '#/components/schemas/GenericSortField' description: Sort field default: -created_at description: Sort field @@ -1199,7 +1199,7 @@ paths: required: false schema: allOf: - - $ref: '#/components/schemas/ProjectSortField' + - $ref: '#/components/schemas/GenericSortField' description: Sort field default: -created_at description: Sort field @@ -12004,10 +12004,17 @@ components: - name - -name title: GenericSortField - description: "Sort options for entity-store-backed list endpoints.\n\nMembers\ - \ map to entity base columns (see ``BASE_FIELDS``), so any\nentity-backed\ - \ resource (filesets, guardrail configs, \u2026) can share this\nwithout advertising\ - \ a sort it cannot honor. Prefix a field with ``-`` for\ndescending order." + description: 'Sort options for entity-store-backed list endpoints. + + + Members map to entity base columns (see ``BASE_FIELDS``), so any + + entity-backed resource (filesets, guardrail configs, workspaces, projects) + + can share this without advertising a sort it cannot honor. Prefix a field + with ``-`` for + + descending order.' GuardrailCheckRequest: properties: model: @@ -16516,17 +16523,6 @@ components: - name title: ProjectInput description: Schema for creating a new project. - ProjectSortField: - type: string - enum: - - created_at - - -created_at - - updated_at - - -updated_at - - name - - -name - title: ProjectSortField - description: Fields available for sorting project results. ProjectUpdate: properties: description: @@ -19652,15 +19648,6 @@ components: - roles title: WorkspaceMemberUpdate description: Schema for updating a workspace member's roles. - WorkspaceSortField: - type: string - enum: - - created_at - - -created_at - - updated_at - - -updated_at - title: WorkspaceSortField - description: Fields available for sorting workspace results. WorkspaceUpdate: properties: description: diff --git a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml index ee0a16bab8..59d1c73080 100644 --- a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml @@ -648,7 +648,6 @@ resources: models: workspace: Workspace workspace_param: WorkspaceInput - workspace_sort_field: WorkspaceSortField workspace_update: WorkspaceUpdate workspaces_page: WorkspacesPage methods: @@ -774,7 +773,6 @@ resources: models: project: Project project_param: ProjectInput - project_sort_field: ProjectSortField project_update: ProjectUpdate projects_page: ProjectsPage methods: diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/workspaces/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/workspaces/__init__.py index d7584d2218..661defd005 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/workspaces/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/workspaces/__init__.py @@ -179,7 +179,7 @@ def list_workspaces( page: Annotated[int | None, typer.Option("--page", help="Page number")] = None, page_size: Annotated[int | None, typer.Option("--page-size", help="Items per page")] = None, sort: Annotated[ - Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None, + Literal["name", "-name", "created_at", "-created_at", "updated_at", "-updated_at"] | None, typer.Option("--sort", help="Sort field"), ] = None, output_format: ListOutputFormatOption = None, diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md index 213f17faf5..7b23f115b5 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md @@ -3,13 +3,7 @@ Types: ```python -from nemo_platform.types.projects import ( - Project, - ProjectParam, - ProjectSortField, - ProjectUpdate, - ProjectsPage, -) +from nemo_platform.types.projects import Project, ProjectParam, ProjectUpdate, ProjectsPage ``` Methods: diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py index ab7eff8672..3adb732e63 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py @@ -31,9 +31,9 @@ ) from ...pagination import SyncDefaultPagination, AsyncDefaultPagination from ..._base_client import AsyncPaginator, make_request_options -from ...types.projects import ProjectSortField, project_list_params, project_create_params, project_update_params +from ...types.projects import project_list_params, project_create_params, project_update_params from ...types.projects.project import Project -from ...types.projects.project_sort_field import ProjectSortField +from ...types.shared.generic_sort_field import GenericSortField from ...types.shared.entity_delete_response import EntityDeleteResponse from ..._exceptions import ConflictError @@ -227,7 +227,7 @@ def list( filter: str | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: ProjectSortField | Omit = omit, + sort: GenericSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -533,7 +533,7 @@ def list( filter: str | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: ProjectSortField | Omit = omit, + sort: GenericSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md index acd2281a32..fdbef6a65c 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md @@ -6,7 +6,6 @@ Types: from nemo_platform.types.workspaces import ( Workspace, WorkspaceParam, - WorkspaceSortField, WorkspaceUpdate, WorkspacesPage, ) diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py index 5ea474ab5b..45144c16c1 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py @@ -39,15 +39,10 @@ ) from ...pagination import SyncDefaultPagination, AsyncDefaultPagination from ..._base_client import AsyncPaginator, make_request_options -from ...types.workspaces import ( - WorkspaceSortField, - workspace_list_params, - workspace_create_params, - workspace_update_params, -) +from ...types.workspaces import workspace_list_params, workspace_create_params, workspace_update_params from ...types.workspaces.workspace import Workspace +from ...types.shared.generic_sort_field import GenericSortField from ...types.shared.entity_delete_response import EntityDeleteResponse -from ...types.workspaces.workspace_sort_field import WorkspaceSortField from ..._exceptions import ConflictError __all__ = ["WorkspacesResource", "AsyncWorkspacesResource"] @@ -242,7 +237,7 @@ def list( filter: str | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: WorkspaceSortField | Omit = omit, + sort: GenericSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -552,7 +547,7 @@ def list( filter: str | Omit = omit, page: int | Omit = omit, page_size: int | Omit = omit, - sort: WorkspaceSortField | Omit = omit, + sort: GenericSortField | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/projects/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/projects/__init__.py index 5b175364b1..90a40b14eb 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/projects/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/projects/__init__.py @@ -19,7 +19,6 @@ from .project import Project as Project from .projects_page import ProjectsPage as ProjectsPage -from .project_sort_field import ProjectSortField as ProjectSortField from .project_list_params import ProjectListParams as ProjectListParams from .project_create_params import ProjectCreateParams as ProjectCreateParams from .project_update_params import ProjectUpdateParams as ProjectUpdateParams diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/projects/project_list_params.py b/sdk/python/nemo-platform/src/nemo_platform/types/projects/project_list_params.py index a4feb80129..0eddd91d65 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/projects/project_list_params.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/projects/project_list_params.py @@ -19,7 +19,7 @@ from typing_extensions import TypedDict -from .project_sort_field import ProjectSortField +from ..shared.generic_sort_field import GenericSortField __all__ = ["ProjectListParams"] @@ -45,5 +45,5 @@ class ProjectListParams(TypedDict, total=False): page_size: int """Items per page""" - sort: ProjectSortField + sort: GenericSortField """Sort field""" diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/projects/project_sort_field.py b/sdk/python/nemo-platform/src/nemo_platform/types/projects/project_sort_field.py deleted file mode 100644 index dd452fe737..0000000000 --- a/sdk/python/nemo-platform/src/nemo_platform/types/projects/project_sort_field.py +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["ProjectSortField"] - -ProjectSortField: TypeAlias = Literal["created_at", "-created_at", "updated_at", "-updated_at", "name", "-name"] diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/__init__.py index 70e365427f..643982631a 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/__init__.py @@ -23,7 +23,6 @@ from .member_create_params import MemberCreateParams as MemberCreateParams from .member_delete_params import MemberDeleteParams as MemberDeleteParams from .member_update_params import MemberUpdateParams as MemberUpdateParams -from .workspace_sort_field import WorkspaceSortField as WorkspaceSortField from .workspace_list_params import WorkspaceListParams as WorkspaceListParams from .workspace_create_params import WorkspaceCreateParams as WorkspaceCreateParams from .workspace_update_params import WorkspaceUpdateParams as WorkspaceUpdateParams diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_list_params.py b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_list_params.py index 07e4cc488e..fbb51a4d6c 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_list_params.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_list_params.py @@ -19,7 +19,7 @@ from typing_extensions import TypedDict -from .workspace_sort_field import WorkspaceSortField +from ..shared.generic_sort_field import GenericSortField __all__ = ["WorkspaceListParams"] @@ -43,5 +43,5 @@ class WorkspaceListParams(TypedDict, total=False): page_size: int """Items per page""" - sort: WorkspaceSortField + sort: GenericSortField """Sort field""" diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_sort_field.py b/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_sort_field.py deleted file mode 100644 index bf9e38101d..0000000000 --- a/sdk/python/nemo-platform/src/nemo_platform/types/workspaces/workspace_sort_field.py +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal, TypeAlias - -__all__ = ["WorkspaceSortField"] - -WorkspaceSortField: TypeAlias = Literal["created_at", "-created_at", "updated_at", "-updated_at"] diff --git a/sdk/python/nemo-platform/tests/api_resources/test_projects.py b/sdk/python/nemo-platform/tests/api_resources/test_projects.py index 9306ef0b4a..8b1564a0a0 100644 --- a/sdk/python/nemo-platform/tests/api_resources/test_projects.py +++ b/sdk/python/nemo-platform/tests/api_resources/test_projects.py @@ -26,9 +26,7 @@ from nemo_platform import NeMoPlatform, AsyncNeMoPlatform from nemo_platform.pagination import SyncDefaultPagination, AsyncDefaultPagination from nemo_platform.types.shared import EntityDeleteResponse -from nemo_platform.types.projects import ( - Project, -) +from nemo_platform.types.projects import Project base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/sdk/stainless.yaml b/sdk/stainless.yaml index ee0a16bab8..59d1c73080 100644 --- a/sdk/stainless.yaml +++ b/sdk/stainless.yaml @@ -648,7 +648,6 @@ resources: models: workspace: Workspace workspace_param: WorkspaceInput - workspace_sort_field: WorkspaceSortField workspace_update: WorkspaceUpdate workspaces_page: WorkspacesPage methods: @@ -774,7 +773,6 @@ resources: models: project: Project project_param: ProjectInput - project_sort_field: ProjectSortField project_update: ProjectUpdate projects_page: ProjectsPage methods: diff --git a/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py b/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py index 28b4aff883..82a501e604 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py @@ -16,12 +16,11 @@ import textwrap from fastapi import APIRouter, HTTPException, Query, status -from nmp.common.api.common import Page, PaginationData +from nmp.common.api.common import GenericSortField, Page, PaginationData from nmp.core.entities.api.dependencies import EntityRepository from nmp.core.entities.api.v2.projects.schemas import ( Project, ProjectInput, - ProjectSortField, ProjectUpdate, ) from nmp.core.entities.api.v2.schemas import EntityDeleteResponse @@ -141,7 +140,7 @@ async def list_projects( filter: FilterDep, page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(100, ge=1, le=1000, description="Items per page"), - sort: ProjectSortField = Query(ProjectSortField.CREATED_AT_DESC, description="Sort field"), + sort: GenericSortField = Query(GenericSortField.CREATED_AT_DESC, description="Sort field"), ) -> Page[Project]: """List projects in the workspace.""" entities, total = await repository.list_entities( diff --git a/services/core/entities/src/nmp/core/entities/api/v2/projects/schemas.py b/services/core/entities/src/nmp/core/entities/api/v2/projects/schemas.py index d82f3651fe..1bbb6ad0f2 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/projects/schemas.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/projects/schemas.py @@ -4,7 +4,6 @@ """Project API Schemas for v2.""" from datetime import datetime -from enum import Enum from typing import Optional from nmp.common.entities.constants import NAME_PATTERN, NAME_PATTERN_DESCRIPTION @@ -42,17 +41,6 @@ class Project(BaseModel): updated_at: datetime = Field(..., description="Last update timestamp") -class ProjectSortField(str, Enum): - """Fields available for sorting project results.""" - - CREATED_AT_ASC = "created_at" - CREATED_AT_DESC = "-created_at" - UPDATED_AT_ASC = "updated_at" - UPDATED_AT_DESC = "-updated_at" - NAME_ASC = "name" - NAME_DESC = "-name" - - class ProjectFilter(BaseModel): """Filter for Projects.""" diff --git a/services/core/entities/src/nmp/core/entities/api/v2/schemas.py b/services/core/entities/src/nmp/core/entities/api/v2/schemas.py index 7fa4b67b9f..5ac1ea6e64 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/schemas.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/schemas.py @@ -3,20 +3,9 @@ """Shared API Schemas for v2.""" -from enum import StrEnum - from pydantic import BaseModel, Field -class WorkspaceSortField(StrEnum): - """Fields available for sorting workspace results.""" - - CREATED_AT_ASC = "created_at" - CREATED_AT_DESC = "-created_at" - UPDATED_AT_ASC = "updated_at" - UPDATED_AT_DESC = "-updated_at" - - class EntityDeleteResponse(BaseModel): """Response for successful delete operations.""" diff --git a/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py b/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py index 06f38569ea..5c1cf7f658 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py @@ -24,11 +24,11 @@ from datetime import datetime, timezone from fastapi import APIRouter, HTTPException, Query, status -from nmp.common.api.common import Page, PaginationData +from nmp.common.api.common import GenericSortField, Page, PaginationData from nmp.common.api.filter import ComparisonOperation, FilterOperator from nmp.common.auth.models import Principal from nmp.core.entities.api.dependencies import AuthClientDep, EntityRepository, WorkspaceRepository -from nmp.core.entities.api.v2.schemas import EntityDeleteResponse, WorkspaceSortField +from nmp.core.entities.api.v2.schemas import EntityDeleteResponse from nmp.core.entities.api.v2.utils import ( ROLE_BINDING_ENTITY_TYPE, add_workspace_filtering, @@ -286,7 +286,7 @@ async def list_workspaces( filter: FilterDep, page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(100, ge=1, le=1000, description="Items per page"), - sort: WorkspaceSortField = Query(WorkspaceSortField.CREATED_AT_DESC, description="Sort field"), + sort: GenericSortField = Query(GenericSortField.CREATED_AT_DESC, description="Sort field"), ) -> Page[Workspace]: """List workspaces accessible to the current principal.""" # Get accessible workspaces for access control From d87fe989ef4add8e809ef67c347145e8713a95bf Mon Sep 17 00:00:00 2001 From: Albert Cui Date: Mon, 20 Jul 2026 17:44:45 -0400 Subject: [PATCH 8/9] refactor(entities): consolidate delete responses into one DeleteResponse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entity/project/workspace delete endpoints returned EntityDeleteResponse (message, id, deleted_count) while guardrails and IAM used the shared nmp.common.api.common.DeleteResponse (message, id, deleted_at) — the name collision the hardening pass resolved by keeping two models. Both extra fields turned out to be write-only (read only by integration tests; no web or SDK consumer), and deleted_count is ~always 1 (delete_entity_by_name returns a hardcoded 1; the handler 404s on 0). So collapse to a single DeleteResponse{message, id} with id required, and point all six delete endpoints (entities, projects, workspaces, members, guardrails, IAM) plus the plugin EntityClient at it. Drop EntityDeleteResponse, deleted_count, and deleted_at. Regenerate the OpenAPI spec and Python SDK; update the integration asserts that checked the removed fields. Signed-off-by: Albert Cui --- openapi/ga/individual/platform.openapi.yaml | 37 ++++--------------- openapi/ga/openapi.yaml | 37 ++++--------------- openapi/openapi.yaml | 37 ++++--------------- .../src/nemo_platform_plugin/entities.py | 10 ++--- .../nmp_common/src/nmp/common/api/common.py | 3 +- .../nemo-platform/.nmpcontext/openapi.yaml | 37 ++++--------------- .../nemo-platform/.nmpcontext/stainless.yaml | 1 - sdk/python/nemo-platform/api.md | 1 - .../nemo_platform/resources/entities/api.md | 2 +- .../resources/entities/entities.py | 10 ++--- .../nemo_platform/resources/projects/api.md | 2 +- .../resources/projects/projects.py | 10 ++--- .../nemo_platform/resources/workspaces/api.md | 4 +- .../resources/workspaces/members.py | 10 ++--- .../resources/workspaces/workspaces.py | 10 ++--- .../src/nemo_platform/types/__init__.py | 1 - .../nemo_platform/types/shared/__init__.py | 1 - .../types/shared/delete_response.py | 6 +-- .../types/shared/entity_delete_response.py | 34 ----------------- .../tests/api_resources/test_entities.py | 18 ++++----- .../tests/api_resources/test_projects.py | 14 +++---- .../tests/api_resources/test_workspaces.py | 14 +++---- .../api_resources/workspaces/test_members.py | 18 ++++----- sdk/stainless.yaml | 1 - .../src/nmp/core/auth/api/v2/iam/endpoints.py | 2 +- .../integration/test_iam_role_bindings.py | 1 - .../entities/api/v2/entities/endpoints.py | 9 ++--- .../entities/api/v2/projects/endpoints.py | 10 ++--- .../src/nmp/core/entities/api/v2/schemas.py | 17 --------- .../entities/api/v2/workspaces/endpoints.py | 17 ++++----- .../integration/test_generic_entities.py | 4 +- .../tests/integration/test_projects.py | 2 +- .../integration/test_workspace_members.py | 1 - 33 files changed, 110 insertions(+), 271 deletions(-) delete mode 100644 sdk/python/nemo-platform/src/nemo_platform/types/shared/entity_delete_response.py delete mode 100644 services/core/entities/src/nmp/core/entities/api/v2/schemas.py diff --git a/openapi/ga/individual/platform.openapi.yaml b/openapi/ga/individual/platform.openapi.yaml index 1a4d8888c2..175812eac9 100644 --- a/openapi/ga/individual/platform.openapi.yaml +++ b/openapi/ga/individual/platform.openapi.yaml @@ -566,7 +566,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -899,7 +899,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -1100,7 +1100,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -1359,7 +1359,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -9932,15 +9932,12 @@ components: title: Message default: Resource deleted successfully. id: + type: string title: Id description: The ID of the deleted resource. - type: string - deleted_at: - title: Deleted At - description: The timestamp when the resource was deleted. - type: string - format: date-time type: object + required: + - id title: DeleteResponse DialogRails: properties: @@ -10383,26 +10380,6 @@ components: Name is optional - if not provided, it will be auto-generated. Workspace and entity_type come from the URL path parameters.' - EntityDeleteResponse: - properties: - message: - type: string - title: Message - default: Resource deleted successfully - id: - type: string - title: Id - description: ID of the deleted resource - deleted_count: - type: integer - title: Deleted Count - description: Number of items deleted - default: 1 - type: object - required: - - id - title: EntityDeleteResponse - description: Response for successful delete operations. EntityUpdate: properties: new_name: diff --git a/openapi/ga/openapi.yaml b/openapi/ga/openapi.yaml index 1a4d8888c2..175812eac9 100644 --- a/openapi/ga/openapi.yaml +++ b/openapi/ga/openapi.yaml @@ -566,7 +566,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -899,7 +899,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -1100,7 +1100,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -1359,7 +1359,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -9932,15 +9932,12 @@ components: title: Message default: Resource deleted successfully. id: + type: string title: Id description: The ID of the deleted resource. - type: string - deleted_at: - title: Deleted At - description: The timestamp when the resource was deleted. - type: string - format: date-time type: object + required: + - id title: DeleteResponse DialogRails: properties: @@ -10383,26 +10380,6 @@ components: Name is optional - if not provided, it will be auto-generated. Workspace and entity_type come from the URL path parameters.' - EntityDeleteResponse: - properties: - message: - type: string - title: Message - default: Resource deleted successfully - id: - type: string - title: Id - description: ID of the deleted resource - deleted_count: - type: integer - title: Deleted Count - description: Number of items deleted - default: 1 - type: object - required: - - id - title: EntityDeleteResponse - description: Response for successful delete operations. EntityUpdate: properties: new_name: diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 1a4d8888c2..175812eac9 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -566,7 +566,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -899,7 +899,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -1100,7 +1100,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -1359,7 +1359,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -9932,15 +9932,12 @@ components: title: Message default: Resource deleted successfully. id: + type: string title: Id description: The ID of the deleted resource. - type: string - deleted_at: - title: Deleted At - description: The timestamp when the resource was deleted. - type: string - format: date-time type: object + required: + - id title: DeleteResponse DialogRails: properties: @@ -10383,26 +10380,6 @@ components: Name is optional - if not provided, it will be auto-generated. Workspace and entity_type come from the URL path parameters.' - EntityDeleteResponse: - properties: - message: - type: string - title: Message - default: Resource deleted successfully - id: - type: string - title: Id - description: ID of the deleted resource - deleted_count: - type: integer - title: Deleted Count - description: Number of items deleted - default: 1 - type: object - required: - - id - title: EntityDeleteResponse - description: Response for successful delete operations. EntityUpdate: properties: new_name: diff --git a/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py b/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py index b51e211d3b..56dde5bd12 100644 --- a/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py +++ b/packages/nemo_platform_plugin/src/nemo_platform_plugin/entities.py @@ -10,7 +10,7 @@ from nemo_platform import ConflictError, NotFoundError, UnprocessableEntityError, omit from nemo_platform.resources.entities import AsyncEntitiesResource -from nemo_platform.types import EntityDeleteResponse +from nemo_platform.types import DeleteResponse from nemo_platform.types.entities import Entity from nemo_platform_plugin.filter_ops import FilterOperation from pydantic import BaseModel, Field, PrivateAttr, TypeAdapter, computed_field @@ -238,8 +238,8 @@ async def get_by_id(self, entity_type: EntityTypeLike, entity_id: str) -> Entity async def update(self, entity: EntityT, *, original_name: str | None = None) -> EntityT: ... async def delete( self, entity_type: EntityTypeLike, name: str, *, workspace: Optional[str] = None - ) -> EntityDeleteResponse: ... - async def delete_by_id(self, entity_type: EntityTypeLike, entity_id: str) -> EntityDeleteResponse: ... + ) -> DeleteResponse: ... + async def delete_by_id(self, entity_type: EntityTypeLike, entity_id: str) -> DeleteResponse: ... async def save(self, entity: EntityT) -> EntityT: ... async def add(self, entity: EntityT) -> EntityT: ... async def get_by_field( @@ -633,7 +633,7 @@ async def delete( *, workspace: Optional[str] = None, parent: Optional[str] = None, - ) -> EntityDeleteResponse: + ) -> DeleteResponse: """Delete an entity by name. Supports workspace-qualified names like "prod/my-model". @@ -665,7 +665,7 @@ async def delete_by_id( self, entity_type: EntityTypeLike, entity_id: str, - ) -> EntityDeleteResponse: + ) -> DeleteResponse: """Delete an entity by ID. First retrieves the entity to get its workspace and name, then deletes by name. diff --git a/packages/nmp_common/src/nmp/common/api/common.py b/packages/nmp_common/src/nmp/common/api/common.py index 9b86244407..69305e462a 100644 --- a/packages/nmp_common/src/nmp/common/api/common.py +++ b/packages/nmp_common/src/nmp/common/api/common.py @@ -77,8 +77,7 @@ class GenericSortField(StrEnum): class DeleteResponse(Value): message: str = Field(default="Resource deleted successfully.") - id: Optional[str] = Field(default=None, description="The ID of the deleted resource.") - deleted_at: Optional[datetime] = Field(default=None, description="The timestamp when the resource was deleted.") + id: str = Field(..., description="The ID of the deleted resource.") class ErrorResponse(Value): diff --git a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml index 1a4d8888c2..175812eac9 100644 --- a/sdk/python/nemo-platform/.nmpcontext/openapi.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/openapi.yaml @@ -566,7 +566,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -899,7 +899,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -1100,7 +1100,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -1359,7 +1359,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/EntityDeleteResponse' + $ref: '#/components/schemas/DeleteResponse' '422': description: Validation Error content: @@ -9932,15 +9932,12 @@ components: title: Message default: Resource deleted successfully. id: + type: string title: Id description: The ID of the deleted resource. - type: string - deleted_at: - title: Deleted At - description: The timestamp when the resource was deleted. - type: string - format: date-time type: object + required: + - id title: DeleteResponse DialogRails: properties: @@ -10383,26 +10380,6 @@ components: Name is optional - if not provided, it will be auto-generated. Workspace and entity_type come from the URL path parameters.' - EntityDeleteResponse: - properties: - message: - type: string - title: Message - default: Resource deleted successfully - id: - type: string - title: Id - description: ID of the deleted resource - deleted_count: - type: integer - title: Deleted Count - description: Number of items deleted - default: 1 - type: object - required: - - id - title: EntityDeleteResponse - description: Response for successful delete operations. EntityUpdate: properties: new_name: diff --git a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml index 59d1c73080..f552111d2c 100644 --- a/sdk/python/nemo-platform/.nmpcontext/stainless.yaml +++ b/sdk/python/nemo-platform/.nmpcontext/stainless.yaml @@ -751,7 +751,6 @@ resources: workload_token_exchange_error_response: WorkloadTokenExchangeErrorResponse json_web_key: JsonWebKey json_web_key_set_response: JsonWebKeySetResponse - entity_delete_response: EntityDeleteResponse iam: standalone_api: true subresources: diff --git a/sdk/python/nemo-platform/api.md b/sdk/python/nemo-platform/api.md index 00c028782a..b235448956 100644 --- a/sdk/python/nemo-platform/api.md +++ b/sdk/python/nemo-platform/api.md @@ -9,7 +9,6 @@ from nemo_platform.types import ( DatasetMetadataContent, DatetimeFilter, DeleteResponse, - EntityDeleteResponse, FileStorageType, FilesetMetadata, FilesetMetadataParam, diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/entities/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/entities/api.md index 86b88c8b92..79b770c00c 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/entities/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/entities/api.md @@ -10,7 +10,7 @@ Methods: - client.entities.create(entity_type, \*, workspace, \*\*params) -> Entity - client.entities.list(entity_type, \*, workspace, \*\*params) -> SyncDefaultPagination[Entity] -- client.entities.delete_entity_by_name(name, \*, workspace, entity_type, \*\*params) -> EntityDeleteResponse +- client.entities.delete_entity_by_name(name, \*, workspace, entity_type, \*\*params) -> DeleteResponse - client.entities.get_entity_by_id(id) -> Entity - client.entities.get_entity_by_name(name, \*, workspace, entity_type, \*\*params) -> Entity - client.entities.update_entity_by_name(name, \*, workspace, entity_type, \*\*params) -> Entity diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/entities/entities.py b/sdk/python/nemo-platform/src/nemo_platform/resources/entities/entities.py index 99902e5805..3375494483 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/entities/entities.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/entities/entities.py @@ -41,7 +41,7 @@ entity_update_entity_by_name_params, ) from ...types.entities.entity import Entity -from ...types.shared.entity_delete_response import EntityDeleteResponse +from ...types.shared.delete_response import DeleteResponse __all__ = ["EntitiesResource", "AsyncEntitiesResource"] @@ -245,7 +245,7 @@ def delete_entity_by_name( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> EntityDeleteResponse: + ) -> DeleteResponse: """ Delete an entity by its name. @@ -290,7 +290,7 @@ def delete_entity_by_name( {"parent": parent}, entity_delete_entity_by_name_params.EntityDeleteEntityByNameParams ), ), - cast_to=EntityDeleteResponse, + cast_to=DeleteResponse, ) def get_entity_by_id( @@ -680,7 +680,7 @@ async def delete_entity_by_name( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> EntityDeleteResponse: + ) -> DeleteResponse: """ Delete an entity by its name. @@ -725,7 +725,7 @@ async def delete_entity_by_name( {"parent": parent}, entity_delete_entity_by_name_params.EntityDeleteEntityByNameParams ), ), - cast_to=EntityDeleteResponse, + cast_to=DeleteResponse, ) async def get_entity_by_id( diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md index 7b23f115b5..7b522f0312 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/api.md @@ -12,4 +12,4 @@ Methods: - client.projects.retrieve(name, \*, workspace) -> Project - client.projects.update(name, \*, workspace, \*\*params) -> Project - client.projects.list(\*, workspace, \*\*params) -> SyncDefaultPagination[Project] -- client.projects.delete(name, \*, workspace) -> EntityDeleteResponse +- client.projects.delete(name, \*, workspace) -> DeleteResponse diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py index 3adb732e63..d55418bfcf 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/projects/projects.py @@ -33,8 +33,8 @@ from ..._base_client import AsyncPaginator, make_request_options from ...types.projects import project_list_params, project_create_params, project_update_params from ...types.projects.project import Project +from ...types.shared.delete_response import DeleteResponse from ...types.shared.generic_sort_field import GenericSortField -from ...types.shared.entity_delete_response import EntityDeleteResponse from ..._exceptions import ConflictError __all__ = ["ProjectsResource", "AsyncProjectsResource"] @@ -312,7 +312,7 @@ def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> EntityDeleteResponse: + ) -> DeleteResponse: """ Delete a project. @@ -342,7 +342,7 @@ def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=EntityDeleteResponse, + cast_to=DeleteResponse, ) @@ -618,7 +618,7 @@ async def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> EntityDeleteResponse: + ) -> DeleteResponse: """ Delete a project. @@ -648,7 +648,7 @@ async def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=EntityDeleteResponse, + cast_to=DeleteResponse, ) diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md index fdbef6a65c..bf835b3c96 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/api.md @@ -17,7 +17,7 @@ Methods: - client.workspaces.retrieve(name) -> Workspace - client.workspaces.update(name, \*\*params) -> Workspace - client.workspaces.list(\*\*params) -> SyncDefaultPagination[Workspace] -- client.workspaces.delete(name) -> EntityDeleteResponse +- client.workspaces.delete(name) -> DeleteResponse ## Members @@ -37,4 +37,4 @@ Methods: - client.workspaces.members.create(\*, workspace, \*\*params) -> WorkspaceMember - client.workspaces.members.update(principal_id, \*, workspace, \*\*params) -> WorkspaceMember - client.workspaces.members.list(\*, workspace) -> WorkspaceMemberListResponse -- client.workspaces.members.delete(principal_id, \*, workspace, \*\*params) -> EntityDeleteResponse +- client.workspaces.members.delete(principal_id, \*, workspace, \*\*params) -> DeleteResponse diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/members.py b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/members.py index 9a48463c59..a604919639 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/members.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/members.py @@ -31,8 +31,8 @@ ) from ..._base_client import make_request_options from ...types.workspaces import member_create_params, member_delete_params, member_update_params +from ...types.shared.delete_response import DeleteResponse from ...types.workspaces.workspace_member import WorkspaceMember -from ...types.shared.entity_delete_response import EntityDeleteResponse from ...types.workspaces.workspace_member_list_response import WorkspaceMemberListResponse __all__ = ["MembersResource", "AsyncMembersResource"] @@ -249,7 +249,7 @@ def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> EntityDeleteResponse: + ) -> DeleteResponse: """ Remove a member from the workspace by revoking all their roles. @@ -296,7 +296,7 @@ def delete( {"wait_role_propagation": wait_role_propagation}, member_delete_params.MemberDeleteParams ), ), - cast_to=EntityDeleteResponse, + cast_to=DeleteResponse, ) @@ -511,7 +511,7 @@ async def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> EntityDeleteResponse: + ) -> DeleteResponse: """ Remove a member from the workspace by revoking all their roles. @@ -558,7 +558,7 @@ async def delete( {"wait_role_propagation": wait_role_propagation}, member_delete_params.MemberDeleteParams ), ), - cast_to=EntityDeleteResponse, + cast_to=DeleteResponse, ) diff --git a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py index 45144c16c1..1144266e61 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py +++ b/sdk/python/nemo-platform/src/nemo_platform/resources/workspaces/workspaces.py @@ -41,8 +41,8 @@ from ..._base_client import AsyncPaginator, make_request_options from ...types.workspaces import workspace_list_params, workspace_create_params, workspace_update_params from ...types.workspaces.workspace import Workspace +from ...types.shared.delete_response import DeleteResponse from ...types.shared.generic_sort_field import GenericSortField -from ...types.shared.entity_delete_response import EntityDeleteResponse from ..._exceptions import ConflictError __all__ = ["WorkspacesResource", "AsyncWorkspacesResource"] @@ -320,7 +320,7 @@ def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> EntityDeleteResponse: + ) -> DeleteResponse: """ Delete a workspace. @@ -352,7 +352,7 @@ def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=EntityDeleteResponse, + cast_to=DeleteResponse, ) @@ -630,7 +630,7 @@ async def delete( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> EntityDeleteResponse: + ) -> DeleteResponse: """ Delete a workspace. @@ -662,7 +662,7 @@ async def delete( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=EntityDeleteResponse, + cast_to=DeleteResponse, ) diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/__init__.py index 904c057a6c..0dc61f81ec 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/__init__.py @@ -43,7 +43,6 @@ PlatformJobLogPage as PlatformJobLogPage, HTTPValidationError as HTTPValidationError, SlidingWindowConfig as SlidingWindowConfig, - EntityDeleteResponse as EntityDeleteResponse, FilesetMetadataParam as FilesetMetadataParam, ModelMetadataContent as ModelMetadataContent, AuthDiscoveryResponse as AuthDiscoveryResponse, diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/shared/__init__.py b/sdk/python/nemo-platform/src/nemo_platform/types/shared/__init__.py index 785c6d6519..e89faaf631 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/shared/__init__.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/shared/__init__.py @@ -40,7 +40,6 @@ from .http_validation_error import HTTPValidationError as HTTPValidationError from .platform_job_log_page import PlatformJobLogPage as PlatformJobLogPage from .sliding_window_config import SlidingWindowConfig as SlidingWindowConfig -from .entity_delete_response import EntityDeleteResponse as EntityDeleteResponse from .fileset_metadata_param import FilesetMetadataParam as FilesetMetadataParam from .model_metadata_content import ModelMetadataContent as ModelMetadataContent from .auth_discovery_response import AuthDiscoveryResponse as AuthDiscoveryResponse diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/shared/delete_response.py b/sdk/python/nemo-platform/src/nemo_platform/types/shared/delete_response.py index 93374583cd..1ad79f3a9c 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/types/shared/delete_response.py +++ b/sdk/python/nemo-platform/src/nemo_platform/types/shared/delete_response.py @@ -16,7 +16,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional -from datetime import datetime from ..._models import BaseModel @@ -24,10 +23,7 @@ class DeleteResponse(BaseModel): - id: Optional[str] = None + id: str """The ID of the deleted resource.""" - deleted_at: Optional[datetime] = None - """The timestamp when the resource was deleted.""" - message: Optional[str] = None diff --git a/sdk/python/nemo-platform/src/nemo_platform/types/shared/entity_delete_response.py b/sdk/python/nemo-platform/src/nemo_platform/types/shared/entity_delete_response.py deleted file mode 100644 index 7c4d604967..0000000000 --- a/sdk/python/nemo-platform/src/nemo_platform/types/shared/entity_delete_response.py +++ /dev/null @@ -1,34 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel - -__all__ = ["EntityDeleteResponse"] - - -class EntityDeleteResponse(BaseModel): - """Response for successful delete operations.""" - - id: str - """ID of the deleted resource""" - - deleted_count: Optional[int] = None - """Number of items deleted""" - - message: Optional[str] = None diff --git a/sdk/python/nemo-platform/tests/api_resources/test_entities.py b/sdk/python/nemo-platform/tests/api_resources/test_entities.py index bb0833370d..a76aaf406b 100644 --- a/sdk/python/nemo-platform/tests/api_resources/test_entities.py +++ b/sdk/python/nemo-platform/tests/api_resources/test_entities.py @@ -25,7 +25,7 @@ from tests.utils import assert_matches_type from nemo_platform import NeMoPlatform, AsyncNeMoPlatform from nemo_platform.pagination import SyncDefaultPagination, AsyncDefaultPagination -from nemo_platform.types.shared import EntityDeleteResponse +from nemo_platform.types.shared import DeleteResponse from nemo_platform.types.entities import ( Entity, ) @@ -179,7 +179,7 @@ def test_method_delete_entity_by_name(self, client: NeMoPlatform) -> None: workspace="workspace", entity_type="entity_type", ) - assert_matches_type(EntityDeleteResponse, entity, path=["response"]) + assert_matches_type(DeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -190,7 +190,7 @@ def test_method_delete_entity_by_name_with_all_params(self, client: NeMoPlatform entity_type="entity_type", parent="parent", ) - assert_matches_type(EntityDeleteResponse, entity, path=["response"]) + assert_matches_type(DeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -204,7 +204,7 @@ def test_raw_response_delete_entity_by_name(self, client: NeMoPlatform) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" entity = response.parse() - assert_matches_type(EntityDeleteResponse, entity, path=["response"]) + assert_matches_type(DeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -218,7 +218,7 @@ def test_streaming_response_delete_entity_by_name(self, client: NeMoPlatform) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" entity = response.parse() - assert_matches_type(EntityDeleteResponse, entity, path=["response"]) + assert_matches_type(DeleteResponse, entity, path=["response"]) assert cast(Any, response.is_closed) is True @@ -597,7 +597,7 @@ async def test_method_delete_entity_by_name(self, async_client: AsyncNeMoPlatfor workspace="workspace", entity_type="entity_type", ) - assert_matches_type(EntityDeleteResponse, entity, path=["response"]) + assert_matches_type(DeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -608,7 +608,7 @@ async def test_method_delete_entity_by_name_with_all_params(self, async_client: entity_type="entity_type", parent="parent", ) - assert_matches_type(EntityDeleteResponse, entity, path=["response"]) + assert_matches_type(DeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -622,7 +622,7 @@ async def test_raw_response_delete_entity_by_name(self, async_client: AsyncNeMoP assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" entity = await response.parse() - assert_matches_type(EntityDeleteResponse, entity, path=["response"]) + assert_matches_type(DeleteResponse, entity, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -636,7 +636,7 @@ async def test_streaming_response_delete_entity_by_name(self, async_client: Asyn assert response.http_request.headers.get("X-Stainless-Lang") == "python" entity = await response.parse() - assert_matches_type(EntityDeleteResponse, entity, path=["response"]) + assert_matches_type(DeleteResponse, entity, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/sdk/python/nemo-platform/tests/api_resources/test_projects.py b/sdk/python/nemo-platform/tests/api_resources/test_projects.py index 8b1564a0a0..44932162be 100644 --- a/sdk/python/nemo-platform/tests/api_resources/test_projects.py +++ b/sdk/python/nemo-platform/tests/api_resources/test_projects.py @@ -25,7 +25,7 @@ from tests.utils import assert_matches_type from nemo_platform import NeMoPlatform, AsyncNeMoPlatform from nemo_platform.pagination import SyncDefaultPagination, AsyncDefaultPagination -from nemo_platform.types.shared import EntityDeleteResponse +from nemo_platform.types.shared import DeleteResponse from nemo_platform.types.projects import Project base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -265,7 +265,7 @@ def test_method_delete(self, client: NeMoPlatform) -> None: name="name", workspace="workspace", ) - assert_matches_type(EntityDeleteResponse, project, path=["response"]) + assert_matches_type(DeleteResponse, project, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -278,7 +278,7 @@ def test_raw_response_delete(self, client: NeMoPlatform) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(EntityDeleteResponse, project, path=["response"]) + assert_matches_type(DeleteResponse, project, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -291,7 +291,7 @@ def test_streaming_response_delete(self, client: NeMoPlatform) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = response.parse() - assert_matches_type(EntityDeleteResponse, project, path=["response"]) + assert_matches_type(DeleteResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True @@ -547,7 +547,7 @@ async def test_method_delete(self, async_client: AsyncNeMoPlatform) -> None: name="name", workspace="workspace", ) - assert_matches_type(EntityDeleteResponse, project, path=["response"]) + assert_matches_type(DeleteResponse, project, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -560,7 +560,7 @@ async def test_raw_response_delete(self, async_client: AsyncNeMoPlatform) -> Non assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(EntityDeleteResponse, project, path=["response"]) + assert_matches_type(DeleteResponse, project, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -573,7 +573,7 @@ async def test_streaming_response_delete(self, async_client: AsyncNeMoPlatform) assert response.http_request.headers.get("X-Stainless-Lang") == "python" project = await response.parse() - assert_matches_type(EntityDeleteResponse, project, path=["response"]) + assert_matches_type(DeleteResponse, project, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/sdk/python/nemo-platform/tests/api_resources/test_workspaces.py b/sdk/python/nemo-platform/tests/api_resources/test_workspaces.py index 77f0149dd1..03b4497a27 100644 --- a/sdk/python/nemo-platform/tests/api_resources/test_workspaces.py +++ b/sdk/python/nemo-platform/tests/api_resources/test_workspaces.py @@ -25,7 +25,7 @@ from tests.utils import assert_matches_type from nemo_platform import NeMoPlatform, AsyncNeMoPlatform from nemo_platform.pagination import SyncDefaultPagination, AsyncDefaultPagination -from nemo_platform.types.shared import EntityDeleteResponse +from nemo_platform.types.shared import DeleteResponse from nemo_platform.types.workspaces import ( Workspace, ) @@ -218,7 +218,7 @@ def test_method_delete(self, client: NeMoPlatform) -> None: workspace = client.workspaces.delete( "name", ) - assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) + assert_matches_type(DeleteResponse, workspace, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -230,7 +230,7 @@ def test_raw_response_delete(self, client: NeMoPlatform) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" workspace = response.parse() - assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) + assert_matches_type(DeleteResponse, workspace, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -242,7 +242,7 @@ def test_streaming_response_delete(self, client: NeMoPlatform) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" workspace = response.parse() - assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) + assert_matches_type(DeleteResponse, workspace, path=["response"]) assert cast(Any, response.is_closed) is True @@ -442,7 +442,7 @@ async def test_method_delete(self, async_client: AsyncNeMoPlatform) -> None: workspace = await async_client.workspaces.delete( "name", ) - assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) + assert_matches_type(DeleteResponse, workspace, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -454,7 +454,7 @@ async def test_raw_response_delete(self, async_client: AsyncNeMoPlatform) -> Non assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" workspace = await response.parse() - assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) + assert_matches_type(DeleteResponse, workspace, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -466,7 +466,7 @@ async def test_streaming_response_delete(self, async_client: AsyncNeMoPlatform) assert response.http_request.headers.get("X-Stainless-Lang") == "python" workspace = await response.parse() - assert_matches_type(EntityDeleteResponse, workspace, path=["response"]) + assert_matches_type(DeleteResponse, workspace, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/sdk/python/nemo-platform/tests/api_resources/workspaces/test_members.py b/sdk/python/nemo-platform/tests/api_resources/workspaces/test_members.py index 23302f48f4..f0455784ec 100644 --- a/sdk/python/nemo-platform/tests/api_resources/workspaces/test_members.py +++ b/sdk/python/nemo-platform/tests/api_resources/workspaces/test_members.py @@ -24,7 +24,7 @@ from tests.utils import assert_matches_type from nemo_platform import NeMoPlatform, AsyncNeMoPlatform -from nemo_platform.types.shared import EntityDeleteResponse +from nemo_platform.types.shared import DeleteResponse from nemo_platform.types.workspaces import ( WorkspaceMember, WorkspaceMemberListResponse, @@ -210,7 +210,7 @@ def test_method_delete(self, client: NeMoPlatform) -> None: principal_id="principal_id", workspace="workspace", ) - assert_matches_type(EntityDeleteResponse, member, path=["response"]) + assert_matches_type(DeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -220,7 +220,7 @@ def test_method_delete_with_all_params(self, client: NeMoPlatform) -> None: workspace="workspace", wait_role_propagation=True, ) - assert_matches_type(EntityDeleteResponse, member, path=["response"]) + assert_matches_type(DeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -233,7 +233,7 @@ def test_raw_response_delete(self, client: NeMoPlatform) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(EntityDeleteResponse, member, path=["response"]) + assert_matches_type(DeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -246,7 +246,7 @@ def test_streaming_response_delete(self, client: NeMoPlatform) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(EntityDeleteResponse, member, path=["response"]) + assert_matches_type(DeleteResponse, member, path=["response"]) assert cast(Any, response.is_closed) is True @@ -445,7 +445,7 @@ async def test_method_delete(self, async_client: AsyncNeMoPlatform) -> None: principal_id="principal_id", workspace="workspace", ) - assert_matches_type(EntityDeleteResponse, member, path=["response"]) + assert_matches_type(DeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -455,7 +455,7 @@ async def test_method_delete_with_all_params(self, async_client: AsyncNeMoPlatfo workspace="workspace", wait_role_propagation=True, ) - assert_matches_type(EntityDeleteResponse, member, path=["response"]) + assert_matches_type(DeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -468,7 +468,7 @@ async def test_raw_response_delete(self, async_client: AsyncNeMoPlatform) -> Non assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(EntityDeleteResponse, member, path=["response"]) + assert_matches_type(DeleteResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -481,7 +481,7 @@ async def test_streaming_response_delete(self, async_client: AsyncNeMoPlatform) assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(EntityDeleteResponse, member, path=["response"]) + assert_matches_type(DeleteResponse, member, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/sdk/stainless.yaml b/sdk/stainless.yaml index 59d1c73080..f552111d2c 100644 --- a/sdk/stainless.yaml +++ b/sdk/stainless.yaml @@ -751,7 +751,6 @@ resources: workload_token_exchange_error_response: WorkloadTokenExchangeErrorResponse json_web_key: JsonWebKey json_web_key_set_response: JsonWebKeySetResponse - entity_delete_response: EntityDeleteResponse iam: standalone_api: true subresources: diff --git a/services/core/auth/src/nmp/core/auth/api/v2/iam/endpoints.py b/services/core/auth/src/nmp/core/auth/api/v2/iam/endpoints.py index e3d4298679..bbaa9168ba 100644 --- a/services/core/auth/src/nmp/core/auth/api/v2/iam/endpoints.py +++ b/services/core/auth/src/nmp/core/auth/api/v2/iam/endpoints.py @@ -245,4 +245,4 @@ async def revoke_role_binding( else: logger.warning(f"Timeout waiting for role '{obj.role}' to be revoked for {obj.principal}") - return DeleteResponse(id=obj.id, deleted_at=obj.revoked_at) + return DeleteResponse(id=obj.id) diff --git a/services/core/auth/tests/integration/test_iam_role_bindings.py b/services/core/auth/tests/integration/test_iam_role_bindings.py index e45ecf04e2..43216bd371 100644 --- a/services/core/auth/tests/integration/test_iam_role_bindings.py +++ b/services/core/auth/tests/integration/test_iam_role_bindings.py @@ -79,7 +79,6 @@ def test_role_binding_crud_lifecycle(self, http_client: TestClient): assert response.status_code == 200, f"Delete failed: {response.text}" deleted = response.json() assert deleted["id"] == binding_id - assert "deleted_at" in deleted # Verify revoked (should still exist but be revoked) response = http_client.get(f"/apis/auth/v2/iam/role-bindings/{binding_name}", headers=headers) diff --git a/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py b/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py index a766289eb8..80d8842e3a 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/entities/endpoints.py @@ -22,7 +22,7 @@ import textwrap from fastapi import APIRouter, HTTPException, Query, status -from nmp.common.api.common import Page, PaginationData +from nmp.common.api.common import DeleteResponse, Page, PaginationData from nmp.common.auth import ALL_WORKSPACES from nmp.common.auth.client import AuthClient from nmp.core.entities.api.dependencies import ( @@ -31,7 +31,6 @@ WorkspaceRepository, ) from nmp.core.entities.api.v2.entities.schemas import EntityCreateInput, EntityUpdate -from nmp.core.entities.api.v2.schemas import EntityDeleteResponse from nmp.core.entities.api.v2.utils import ( ROLE_BINDING_ENTITY_TYPE, add_workspace_filtering, @@ -561,7 +560,7 @@ async def update_entity_by_name( @router.delete( "/v2/workspaces/{workspace}/entities/{entity_type}/{name}", - response_model=EntityDeleteResponse, + response_model=DeleteResponse, tags=[API_TAG], summary="Delete entity by name", description=textwrap.dedent(""" @@ -581,7 +580,7 @@ async def delete_entity_by_name( workspace_repository: WorkspaceRepository, auth_client: AuthClientDep, parent: str | None = Query(default=None, description="Parent entity ID for nested entities"), -) -> EntityDeleteResponse: +) -> DeleteResponse: """Delete entity by name.""" # Check if workspace is being deleted (404 for user requests) await validate_workspace_not_deleting(workspace_repository, auth_client, workspace) @@ -601,7 +600,7 @@ async def delete_entity_by_name( ) if deleted_count == 0: raise HTTPException(status_code=404, detail="Entity not found") - return EntityDeleteResponse(id=f"{workspace}/{entity_type}/{name}", deleted_count=deleted_count) + return DeleteResponse(id=f"{workspace}/{entity_type}/{name}") @router.get( diff --git a/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py b/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py index 82a501e604..f01bc78c27 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/projects/endpoints.py @@ -16,14 +16,13 @@ import textwrap from fastapi import APIRouter, HTTPException, Query, status -from nmp.common.api.common import GenericSortField, Page, PaginationData +from nmp.common.api.common import DeleteResponse, GenericSortField, Page, PaginationData from nmp.core.entities.api.dependencies import EntityRepository from nmp.core.entities.api.v2.projects.schemas import ( Project, ProjectInput, ProjectUpdate, ) -from nmp.core.entities.api.v2.schemas import EntityDeleteResponse from nmp.core.entities.app.repository.exceptions import ( EntityNotFoundError, EntityVersionConflictError, @@ -254,7 +253,7 @@ async def update_project( @router.delete( "/v2/workspaces/{workspace}/projects/{name}", - response_model=EntityDeleteResponse, + response_model=DeleteResponse, tags=[API_TAG], summary="Delete project", description=textwrap.dedent(""" @@ -270,7 +269,7 @@ async def delete_project( workspace: str, name: str, repository: EntityRepository, -) -> EntityDeleteResponse: +) -> DeleteResponse: """Delete project.""" try: deleted_count = await repository.delete_entity_by_name( @@ -283,10 +282,9 @@ async def delete_project( status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found in workspace '{workspace}'", ) - return EntityDeleteResponse( + return DeleteResponse( id=f"{workspace}/{name}", message="Project deleted successfully", - deleted_count=deleted_count, ) except IntegrityError as e: error_msg = str(e.orig) if hasattr(e, "orig") else str(e) diff --git a/services/core/entities/src/nmp/core/entities/api/v2/schemas.py b/services/core/entities/src/nmp/core/entities/api/v2/schemas.py deleted file mode 100644 index 5ac1ea6e64..0000000000 --- a/services/core/entities/src/nmp/core/entities/api/v2/schemas.py +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Shared API Schemas for v2.""" - -from pydantic import BaseModel, Field - - -class EntityDeleteResponse(BaseModel): - """Response for successful delete operations.""" - - message: str = Field(default="Resource deleted successfully") - id: str = Field(..., description="ID of the deleted resource") - deleted_count: int = Field( - default=1, - description="Number of items deleted", - ) diff --git a/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py b/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py index 5c1cf7f658..57f6e1ac66 100644 --- a/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py +++ b/services/core/entities/src/nmp/core/entities/api/v2/workspaces/endpoints.py @@ -24,11 +24,10 @@ from datetime import datetime, timezone from fastapi import APIRouter, HTTPException, Query, status -from nmp.common.api.common import GenericSortField, Page, PaginationData +from nmp.common.api.common import DeleteResponse, GenericSortField, Page, PaginationData from nmp.common.api.filter import ComparisonOperation, FilterOperator from nmp.common.auth.models import Principal from nmp.core.entities.api.dependencies import AuthClientDep, EntityRepository, WorkspaceRepository -from nmp.core.entities.api.v2.schemas import EntityDeleteResponse from nmp.core.entities.api.v2.utils import ( ROLE_BINDING_ENTITY_TYPE, add_workspace_filtering, @@ -395,7 +394,7 @@ async def update_workspace( @router.delete( "/v2/workspaces/{name}", - response_model=EntityDeleteResponse, + response_model=DeleteResponse, tags=[API_TAG], summary="Delete workspace", description=textwrap.dedent(""" @@ -417,7 +416,7 @@ async def delete_workspace( name: str, repository: WorkspaceRepository, entity_repository: EntityRepository, -) -> EntityDeleteResponse: +) -> DeleteResponse: """Mark workspace for deletion.""" # Check if workspace exists first workspace = await repository.get_workspace_by_name(name=name) @@ -456,10 +455,9 @@ async def delete_workspace( extra={"workspace": name, "deleted_count": len(deleted_bindings)}, ) - return EntityDeleteResponse( + return DeleteResponse( id=name, message="Workspace marked for deletion", - deleted_count=1, ) @@ -818,7 +816,7 @@ async def update_workspace_member( @router.delete( "/v2/workspaces/{workspace}/members/{principal_id}", - response_model=EntityDeleteResponse, + response_model=DeleteResponse, tags=[API_TAG], summary="Remove workspace member", description=textwrap.dedent(""" @@ -844,7 +842,7 @@ async def remove_workspace_member( default=True, description="If true, wait for roles to propagate before returning (default: true). Set to false for bulk operations.", ), -) -> EntityDeleteResponse: +) -> DeleteResponse: """Remove a member from the workspace.""" # Check if workspace exists ws = await workspace_repository.get_workspace_by_name(name=workspace) @@ -920,8 +918,7 @@ async def remove_workspace_member( extra={"workspace": workspace, "principal": principal_id, "role": role}, ) - return EntityDeleteResponse( + return DeleteResponse( id=principal_id, message=f"Member removed from workspace '{workspace}'", - deleted_count=len(active_bindings), ) diff --git a/services/core/entities/tests/integration/test_generic_entities.py b/services/core/entities/tests/integration/test_generic_entities.py index 02d552bb7b..6175090916 100644 --- a/services/core/entities/tests/integration/test_generic_entities.py +++ b/services/core/entities/tests/integration/test_generic_entities.py @@ -190,7 +190,7 @@ async def test_delete_entity_by_name(self, client: AsyncClient, ctx): assert response.status_code == 200 result = response.json() - assert result["deleted_count"] == 1 + assert "id" in result get_response = await client.get("/apis/entities/v2/workspaces/default/entities/customization_config/delete-me") assert get_response.status_code == 404 @@ -506,7 +506,7 @@ async def test_delete_entity_by_name_with_parent(self, client: AsyncClient, ctx) params={"parent": parent_id}, ) assert response.status_code == 200 - assert response.json()["deleted_count"] == 1 + assert "id" in response.json() # Verify root entity still exists root_response = await client.get("/apis/entities/v2/workspaces/default/entities/child_type/delete-child") diff --git a/services/core/entities/tests/integration/test_projects.py b/services/core/entities/tests/integration/test_projects.py index 6e8119c2e3..e42dfa14f7 100644 --- a/services/core/entities/tests/integration/test_projects.py +++ b/services/core/entities/tests/integration/test_projects.py @@ -141,7 +141,7 @@ async def test_delete_project(self, client: AsyncClient, ctx): assert response.status_code == 200 result = response.json() - assert result["deleted_count"] == 1 + assert "id" in result get_response = await client.get("/apis/entities/v2/workspaces/default/projects/delete-me") assert get_response.status_code == 404 diff --git a/services/core/entities/tests/integration/test_workspace_members.py b/services/core/entities/tests/integration/test_workspace_members.py index 78346b1ca8..8f00c0e8a5 100644 --- a/services/core/entities/tests/integration/test_workspace_members.py +++ b/services/core/entities/tests/integration/test_workspace_members.py @@ -143,7 +143,6 @@ async def test_remove_member(self, client: AsyncClient, ctx): assert response.status_code == 200 result = response.json() assert result["id"] == "remove-me@example.com" - assert result["deleted_count"] >= 1 # Verify member is no longer listed list_response = await client.get("/apis/entities/v2/workspaces/default/members") From a1a2fe832b38c3a0373633e59db5e68b7ae40050 Mon Sep 17 00:00:00 2001 From: Albert Cui Date: Mon, 20 Jul 2026 18:04:52 -0400 Subject: [PATCH 9/9] refactor(openapi): make strict schema-collision detection unconditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-plugin strict_schema_collisions opt-out (warn-and-collapse) had no users — every collision was fixed and no spec opts out — and it can never be correct: collapsing two differing-content models into one name silently repoints the other's $refs, shipping a wrong contract in the generated SDK. It was migration scaffolding for the lenient->strict flip, now complete. Remove it entirely: drop PluginConfig.strict_schema_collisions and the strict_collisions parameter threaded through tweak_spec / _normalize_refs_and_schema_keys / apply_schema_fixes, collapse the plugin strict/lenient split to a single pass, and delete the customizer's now- redundant pin plus the warn-and-collapse test. Collisions now always raise. No schema or SDK change: the generated spec is byte-identical. Signed-off-by: Albert Cui --- .../nmp_common/src/nmp/common/api/utils.py | 22 +++++----------- .../tests/api/test_utils_openapi_spec.py | 26 +++---------------- plugins/nemo-customizer/pyproject.toml | 5 ---- script/generate_openapi_spec.py | 17 ++++-------- script/openapi_helper/plugin_config.py | 9 ------- 5 files changed, 16 insertions(+), 63 deletions(-) diff --git a/packages/nmp_common/src/nmp/common/api/utils.py b/packages/nmp_common/src/nmp/common/api/utils.py index 8664adc1e2..677032c578 100644 --- a/packages/nmp_common/src/nmp/common/api/utils.py +++ b/packages/nmp_common/src/nmp/common/api/utils.py @@ -148,7 +148,7 @@ def _walk_spec(d: Dict, visitor: Callable[[str, Any, Dict], None]): _walk_spec(item, visitor) -def _normalize_refs_and_schema_keys(spec: Dict, *, strict_collisions: bool = True) -> Dict: +def _normalize_refs_and_schema_keys(spec: Dict) -> Dict: """Normalize all ``$ref`` values and schema dictionary keys. Schema keys are renamed first so that ``$ref`` values can be rewritten @@ -157,12 +157,8 @@ def _normalize_refs_and_schema_keys(spec: Dict, *, strict_collisions: bool = Tru When the content *differs*, two distinct Pydantic models are fighting over one schema name, and keeping one silently makes the other's ``$ref``\\ s point - at the wrong contract. ``strict_collisions`` (the default) raises - ``ValueError`` so the build fails loudly rather than shipping a wrong - contract in the generated SDK. Pass ``strict_collisions=False`` to opt a - spec out — it then logs a warning and keeps the first-seen schema (legacy - warn-and-collapse), for a spec that must tolerate a known pre-existing - collision. + at the wrong contract. This raises ``ValueError`` so the build fails loudly + rather than shipping a wrong contract in the generated SDK. """ schemas = spec["components"]["schemas"] @@ -186,17 +182,13 @@ def _normalize_refs_and_schema_keys(spec: Dict, *, strict_collisions: bool = Tru if not any(schemas[key] == schemas[rep] for rep in reps): reps.append(key) if len(reps) > 1: - message = ( + raise ValueError( f"OpenAPI schema name collision: {sorted(old_keys)} all normalize to " f"'{target}' with differing content. Two distinct Pydantic models share a " f"class name across modules; give them distinct names — rename or dedupe the " f"models, or namespace them (e.g. via a per-backend NamespacedModel base) — so " - f"they emit distinct schema names. A spec that must tolerate the collision can " - f"opt out with strict_collisions=False." + f"they emit distinct schema names." ) - if strict_collisions: - raise ValueError(message) - logger.warning("%s Keeping the first-seen schema.", message) # Iterating ``key_to_target`` preserves ``schemas`` order, so the first raw # key that maps to a given target still wins the collapse below. @@ -356,9 +348,9 @@ def _sort_schemas(spec: Dict) -> Dict: return spec -def tweak_spec(spec: Dict, *, strict_collisions: bool = True) -> Dict: +def tweak_spec(spec: Dict) -> Dict: _walk_spec(spec, _anyof_null_visitor) - spec = _normalize_refs_and_schema_keys(spec, strict_collisions=strict_collisions) + spec = _normalize_refs_and_schema_keys(spec) spec = _split_input_output_schemas(spec) spec = _annotate_string_references(spec) spec = _sync_schema_titles(spec) diff --git a/packages/nmp_common/tests/api/test_utils_openapi_spec.py b/packages/nmp_common/tests/api/test_utils_openapi_spec.py index e05cef4a39..a77cfd497a 100644 --- a/packages/nmp_common/tests/api/test_utils_openapi_spec.py +++ b/packages/nmp_common/tests/api/test_utils_openapi_spec.py @@ -356,32 +356,14 @@ def _collision_spec(): } -def test_tweak_spec_raises_on_collision_by_default(): - """A differing-content schema-name collision fails the build by default, - rather than silently keeping one model and mis-pointing the other's - ``$ref``s (which would ship a wrong contract in the generated SDK).""" +def test_tweak_spec_raises_on_collision(): + """A differing-content schema-name collision fails the build, rather than + silently keeping one model and mis-pointing the other's ``$ref``s (which + would ship a wrong contract in the generated SDK).""" with pytest.raises(ValueError, match="schema name collision"): tweak_spec(_collision_spec()) -def test_tweak_spec_warns_and_collapses_when_opted_out(caplog): - """``strict_collisions=False`` opts a spec out: warn and keep the first-seen - schema (legacy warn-and-collapse) for a spec that must tolerate a known - pre-existing collision.""" - import logging - - with caplog.at_level(logging.WARNING, logger="nmp.common.api.utils"): - result = tweak_spec(_collision_spec(), strict_collisions=False) - - assert "schema name collision" in caplog.text - schemas = result["components"]["schemas"] - assert set(schemas) == {"TrainingSpec"} - # First-seen (automodel) wins the collapse. - assert "finetuning_type" in schemas["TrainingSpec"]["properties"] - ref = result["paths"]["/a"]["post"]["requestBody"]["content"]["application/json"]["schema"]["$ref"] - assert ref == REF + "TrainingSpec" - - def test_tweak_spec_dedups_identical_content_module_qualified_collision(): """Two module-qualified keys with *identical* content dedup to one schema — no raise (the fix must not over-trigger on genuinely-shared shapes).""" diff --git a/plugins/nemo-customizer/pyproject.toml b/plugins/nemo-customizer/pyproject.toml index 083f639be1..70c23e582a 100644 --- a/plugins/nemo-customizer/pyproject.toml +++ b/plugins/nemo-customizer/pyproject.toml @@ -33,11 +33,6 @@ build-backend = "hatchling.build" packages = ["src/nemo_customizer"] [tool.nemo.openapi] -# Strict collision detection is the platform-wide default; pinned here because -# nemo-customizer merges every customization backend (automodel, unsloth, …) -# into one /apis/customization app, where two backends emitting the same schema -# name with differing content is always a bug (see NamespacedModel). -strict_schema_collisions = true [tool.uv.sources] diff --git a/script/generate_openapi_spec.py b/script/generate_openapi_spec.py index 141a96be75..969dde1c97 100644 --- a/script/generate_openapi_spec.py +++ b/script/generate_openapi_spec.py @@ -466,7 +466,7 @@ def extract_plugin_specs_with_process_pool(plugins: List[PluginConfig]) -> None: print_green(f"All {len(plugins)} plugin(s) completed successfully!") -def apply_schema_fixes(spec_files: List[str], apply_reorder: bool = True, strict_collisions: bool = True) -> None: +def apply_schema_fixes(spec_files: List[str], apply_reorder: bool = True) -> None: """Apply schema fixes to a list of OpenAPI spec files.""" print_green("=== Applying fixes to OpenAPI schemas ===") # Endpoints stripped from the public OpenAPI spec (not exposed in SDK). @@ -517,7 +517,7 @@ def apply_schema_fixes(spec_files: List[str], apply_reorder: bool = True, strict spec = fix_openai_streaming_endpoints(spec) # Apply the standard fix-schema logic - spec = tweak_spec(spec, strict_collisions=strict_collisions) + spec = tweak_spec(spec) spec = hoist_nested_defs(spec) spec = remove_unused_schemas(spec) spec = remove_invalid_components(spec) @@ -748,16 +748,9 @@ def process_plugin_specs() -> None: # gated on `"platform" in spec_file` are no-ops because plugin paths live # under plugins//openapi/ — no false positives by inspection. # - # Schema-name collisions fail the build by default (strict). A plugin may - # opt OUT via [tool.nemo.openapi].strict_schema_collisions = false if its - # spec must tolerate a known pre-existing collision. Plugin specs are - # independent files, so processing the two groups separately is safe. - strict_spec_files = [p.output_path() for p in plugins if p.strict_schema_collisions] - lenient_spec_files = [p.output_path() for p in plugins if not p.strict_schema_collisions] - if lenient_spec_files: - apply_schema_fixes(lenient_spec_files, strict_collisions=False) - if strict_spec_files: - apply_schema_fixes(strict_spec_files, strict_collisions=True) + # Schema-name collisions fail the build unconditionally: two distinct models + # sharing a schema name with differing content is always a bug. + apply_schema_fixes(plugin_spec_files) fix_ref_not_allowed_errors(plugin_spec_files) validate_final_specs(plugin_spec_files) diff --git a/script/openapi_helper/plugin_config.py b/script/openapi_helper/plugin_config.py index 36816135dd..79bc312412 100644 --- a/script/openapi_helper/plugin_config.py +++ b/script/openapi_helper/plugin_config.py @@ -22,11 +22,6 @@ class PluginConfig: env_vars: Optional[Dict[str, str]] = None factory_override: Optional[str] = None # "module:callable" escape hatch data_designer_plugin_allowlist: Optional[List[str]] = None - # Schema-name collisions fail spec generation by default. Set to false to - # opt a plugin out (warn-and-collapse) if its spec must tolerate a known - # pre-existing collision — collapsing silently ships a wrong contract, so - # opting out should be rare and deliberate. - strict_schema_collisions: bool = True @classmethod def from_pyproject(cls, pyproject_path: Path) -> Optional["PluginConfig"]: @@ -54,9 +49,6 @@ def from_pyproject(cls, pyproject_path: Path) -> Optional["PluginConfig"]: raise ValueError( f"plugin '{plugin_dir}': [tool.nemo.openapi].data_designer_plugin_allowlist must be a list of strings" ) - strict_schema_collisions = opts.get("strict_schema_collisions", True) - if not isinstance(strict_schema_collisions, bool): - raise ValueError(f"plugin '{plugin_dir}': [tool.nemo.openapi].strict_schema_collisions must be a boolean") return cls( dir=plugin_dir, @@ -64,7 +56,6 @@ def from_pyproject(cls, pyproject_path: Path) -> Optional["PluginConfig"]: env_vars=env_vars, factory_override=opts.get("factory_override"), data_designer_plugin_allowlist=data_designer_plugin_allowlist, - strict_schema_collisions=strict_schema_collisions, ) def resolve_service_name(self) -> str: