diff --git a/.github/actions/build-nemo-platform-wheel/action.yaml b/.github/actions/build-nemo-platform-wheel/action.yaml index 4fb5ef1132..3d18d75975 100644 --- a/.github/actions/build-nemo-platform-wheel/action.yaml +++ b/.github/actions/build-nemo-platform-wheel/action.yaml @@ -43,9 +43,9 @@ inputs: required: false default: "19700101000000" python-version: - description: Python version for setup-uv. + description: Python version for setup-uv. Must be within the wheel's supported range. required: false - default: "3.11" + default: "3.12" source-root: description: > Path to the checked-out repository. release-bundle.yaml uses `source`; diff --git a/README.md b/README.md index 710251ebaf..41a0286104 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,13 @@ Quick install from PyPI: ```bash curl -LsSf https://astral.sh/uv/0.9.30/install.sh | sh export PATH="$HOME/.local/bin:$PATH" -uv venv --python 3.13 -source .venv/bin/activate -uv pip install nemo-platform +uv tool install "nemo-platform[all]" nemo setup ``` +`uv tool install` gives you a global `nemo` command in its own isolated environment, with nothing to activate. The `all` extra adds the platform services, so `nemo services run` works; without it you get the SDK and CLI only. To import the SDK from your own code, `pip install "nemo-platform[all]"` into a virtual environment instead. + Source checkout for development: ```bash diff --git a/SETUP.md b/SETUP.md index 7610a8ca69..57b2a85d9d 100644 --- a/SETUP.md +++ b/SETUP.md @@ -86,7 +86,9 @@ is unavailable, start Docker and retry—do not proceed to `rm -rf`. ## Bootstrap and start -The README documents the streamlined path. Prefer this over the manual steps below whenever the task fits — it covers prerequisites install, service startup, provider registration, default/fast model selection, and demo agent deployment in one shot: +This section is the **source checkout** path: use it to work on NeMo Platform itself, on a local plugin, or on Studio assets. To only *use* the platform, install the published wheel instead — `uv tool install "nemo-platform[all]"` needs no checkout and no toolchain, then continue at `nemo setup`. + +The steps below cover prerequisites install, service startup, provider registration, default/fast model selection, and demo agent deployment in one shot. Prefer them over the manual sections further down whenever the task fits: Before running `make bootstrap`, install Flox from the [Flox installation guide](https://flox.dev/docs/install-flox/install). Flox is the recommended source-development toolchain and does not need to be activated first. Contributors using a preinstalled host toolchain instead need uv `>=0.9.14`, Node.js `22.23.2`, pnpm `10.34.5`, and a C compiler; they must use `make TOOLCHAIN=system bootstrap`. diff --git a/docs/cli/index.mdx b/docs/cli/index.mdx index 6cc08b25df..571077a4fc 100644 --- a/docs/cli/index.mdx +++ b/docs/cli/index.mdx @@ -28,25 +28,33 @@ If you previously installed the `nemo-microservices` package, uninstall it first pip uninstall nemo-microservices ``` -### Install in a Virtual Environment +### Install -Install the PyPI `nemo-platform` wrapper package in a Python 3.12-3.13 virtual environment. If you are working from a source checkout, run `make bootstrap` from the repository root instead; the generated SDK package under `sdk/python/nemo-platform/pyproject.toml` does not define the wrapper extras. +Install the PyPI `nemo-platform` wrapper package. From a source checkout, run `make bootstrap` from the repository root instead; the generated SDK package under `sdk/python/nemo-platform/pyproject.toml` does not define the wrapper extras. + + + + + +One command, `nemo` available everywhere, nothing to activate. ```bash -pip install "nemo-platform[all]" +uv tool install "nemo-platform[all]" ``` -Or with uv: + + + + +Use when you also import the SDK from your own code. `nemo` works only while the environment is active. ```bash -uv pip install "nemo-platform[all]" +pip install "nemo-platform[all]" ``` - - -When installed in a virtual environment, the `nemo` command is only available when the environment is activated. + - + ### Verify Installation diff --git a/docs/cli/troubleshooting.mdx b/docs/cli/troubleshooting.mdx index 9c5550e029..ef04649ff6 100644 --- a/docs/cli/troubleshooting.mdx +++ b/docs/cli/troubleshooting.mdx @@ -23,6 +23,25 @@ Activate the virtual environment where you installed the package: source /path/to/venv/bin/activate ``` +## Installation Issues + +### Install Fails Building a Dependency + +**Symptoms:** +- `uv tool install "nemo-platform[all]"` fails while building a dependency, often on a missing C header such as `omp.h` + +**Cause:** + +NeMo Platform supports Python 3.12 and 3.13, but uv [picks the interpreter itself](https://docs.astral.sh/uv/concepts/tools/#python-versions), so an install can land on Python 3.14. + +**Solution:** + +Name a supported interpreter. uv downloads one if you do not have it: + +```bash +uv tool install --python 3.13 "nemo-platform[all]" +``` + ## Connection Issues ### Unable to Connect to Server diff --git a/docs/get-started/setup.mdx b/docs/get-started/setup.mdx index d4d5db0d9d..c379e6a942 100644 --- a/docs/get-started/setup.mdx +++ b/docs/get-started/setup.mdx @@ -12,7 +12,7 @@ Install NeMo Platform and start working with AI agents. - Python 3.12-3.13 - An API key from one of: [NVIDIA Build](https://build.nvidia.com), [OpenAI](https://platform.openai.com/api-keys), [Anthropic](https://console.anthropic.com/settings/keys), [Google Gemini](https://aistudio.google.com/apikey), or a local [Ollama](https://ollama.com) instance (no key needed) - 16 GB available disk space and 8 GB RAM -- For the PyPI install: `pip` in a Python environment +- For the PyPI install: [uv](https://docs.astral.sh/uv/) `>=0.9.14`, or `pip` in a Python environment - For the source checkout install: [Git](https://git-scm.com/), GNU Make, [uv](https://docs.astral.sh/uv/) `>=0.9.14`, [Node.js (≥22.23.2, \<23)](https://nodejs.org/en/download) satisfying `web/package.json`, and [pnpm](https://pnpm.io/installation) ## Install @@ -21,7 +21,23 @@ Choose the install path that matches what you want to do. ### Install from PyPI -Use the [PyPI package](https://pypi.org/project/nemo-platform/) for the quickest local install. The `all` extra includes the SDK, CLI, platform services, and default first-party plugins: +Use the [PyPI package](https://pypi.org/project/nemo-platform/) for the quickest local install. The `all` extra includes the SDK, CLI, platform services, and default first-party plugins; drop it for the SDK and CLI alone. + + + + + +One command, `nemo` available everywhere, nothing to activate. + +```bash +uv tool install "nemo-platform[all]" +``` + + + + + +Use when you also import the SDK from your own code. `nemo` works only while the environment is active. ```bash python -m venv .venv @@ -29,13 +45,11 @@ source .venv/bin/activate pip install "nemo-platform[all]" ``` -For SDK and CLI usage only, install the base package: + -```bash -pip install nemo-platform -``` + -After installing `nemo-platform[all]` from PyPI, continue with `nemo setup`. +Continue with `nemo setup`. ### Install from source diff --git a/packages/nemo_platform/README.md b/packages/nemo_platform/README.md index f0be59a175..ee6840acff 100644 --- a/packages/nemo_platform/README.md +++ b/packages/nemo_platform/README.md @@ -21,13 +21,17 @@ NeMo Platform brings NVIDIA NeMo libraries together under one CLI, Python SDK, a **Prerequisites:** Python 3.12–3.13 and an API key for an inference provider (NVIDIA Build, OpenAI, Anthropic, Google Gemini, or a local Ollama instance). -The `nemo-platform` distribution is a convenience wrapper that bundles the SDK, shared runtime packages, default first-party plugins, and platform services into a single wheel. Install just the SDK and CLI, or install everything needed to run the platform locally: +The `nemo-platform` distribution is a convenience wrapper that bundles the SDK, shared runtime packages, default first-party plugins, and platform services into a single wheel. The `all` extra adds the platform services; drop it for the SDK and CLI alone. + +Recommended — one command, `nemo` available everywhere, nothing to activate: ```bash -# SDK + CLI only -pip install nemo-platform +uv tool install "nemo-platform[all]" +``` -# SDK + CLI + all platform services and default plugins (recommended) +Use a virtual environment instead when you also import the SDK from your own code. `nemo` then works only while the environment is active: + +```bash pip install "nemo-platform[all]" ``` diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/services/cli.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/services/cli.py index 5c58af463d..c2b57ae906 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/services/cli.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/services/cli.py @@ -15,6 +15,7 @@ import typer from nemo_platform_ext.cli.core.help_formatter import create_typer_app from nemo_platform_ext.cli.docker_preflight import require_docker_for_default_local +from nemo_platform_ext.local.install import services_extra_install_command from nemo_platform_ext.local.process import ( ForegroundInstanceError, InstanceAlreadyRunningError, @@ -75,10 +76,7 @@ def _require_services_extra() -> None: "Running local platform services needs extra components that aren't installed yet.\n" "\n" "Install them with:\n" - " pip install 'nemo-platform[all]'\n" - "\n" - "On Python 3.14, use:\n" - " PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 pip install 'nemo-platform[all]'\n", + f" {services_extra_install_command()}\n", err=True, ) raise typer.Exit(1) diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/setup.py b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/setup.py index ecfc915d6d..fe4530ee8d 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/setup.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/setup.py @@ -36,6 +36,7 @@ from pydantic import SecretStr from rich import box from rich.console import Console +from rich.markup import escape from rich.panel import Panel from nemo_platform_ext.cli.commands.skills import registry as skills_registry @@ -49,6 +50,7 @@ from nemo_platform_ext.client.tls import client_verify_from_env from nemo_platform_ext.config.config import Config from nemo_platform_ext.config.models import DEFAULT_BASE_URL, ConfigFile, ConfigParams, LocalServicesConfig, NoAuthUser +from nemo_platform_ext.local.install import services_extra_install_command from nemo_platform_ext.local.process import ( PortConflict, check_port_available_for_start, @@ -1154,9 +1156,7 @@ def _maybe_start_services( if importlib.util.find_spec("pyleak") is None: console.print(f"{CROSS} Local services require extra dependencies that aren't installed.") console.print(" Install them with:") - console.print(" [cyan]pip install 'nemo-platform\\[all]'[/cyan]") - console.print(" On Python 3.14, use:") - console.print(" [cyan]PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 pip install 'nemo-platform\\[all]'[/cyan]") + console.print(f" [cyan]{escape(services_extra_install_command())}[/cyan]") raise typer.Exit(1) # Fail before stop/spawn when default local needs Docker (NVBug 6537617). diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/local/install.py b/packages/nemo_platform_ext/src/nemo_platform_ext/local/install.py new file mode 100644 index 0000000000..52b2c7a383 --- /dev/null +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/local/install.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""How this NeMo Platform install was created.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +def services_extra_install_command() -> str: + """Return the command that adds the packaged service dependencies here. + + ``uv tool install`` environments are managed by uv and are not meant to be + edited with pip; re-running the tool install with the extra upgrades them + in place. uv marks such an environment with a ``uv-receipt.toml`` at its + root, which is what distinguishes it from an ordinary virtual environment. + """ + if (Path(sys.prefix) / "uv-receipt.toml").is_file(): + return "uv tool install 'nemo-platform[all]'" + return "pip install 'nemo-platform[all]'" diff --git a/packages/nemo_platform_ext/src/nemo_platform_ext/local/services.py b/packages/nemo_platform_ext/src/nemo_platform_ext/local/services.py index 6af3f5f04f..2edc4ae7ea 100644 --- a/packages/nemo_platform_ext/src/nemo_platform_ext/local/services.py +++ b/packages/nemo_platform_ext/src/nemo_platform_ext/local/services.py @@ -21,6 +21,7 @@ from nemo_platform import AsyncNeMoPlatform, NeMoPlatform from nemo_platform_ext.local import process +from nemo_platform_ext.local.install import services_extra_install_command from nemo_platform_ext.local.transport import ( EMBEDDED_BASE_URL, UDS_BASE_URL, @@ -407,7 +408,7 @@ async def stop_async(self, *, timeout: float = 30.0, force: bool = False) -> pro def require_services_extra() -> None: if importlib.util.find_spec("pyleak") is not None: return - raise ServicesExtraRequiredError("Install service dependencies with `pip install 'nemo-platform[all]'`.") + raise ServicesExtraRequiredError(f"Install service dependencies with `{services_extra_install_command()}`.") def _validate_socket_path_length(socket_path: Path) -> None: diff --git a/packages/nemo_platform_ext/tests/local/test_install.py b/packages/nemo_platform_ext/tests/local/test_install.py new file mode 100644 index 0000000000..7225fa43ef --- /dev/null +++ b/packages/nemo_platform_ext/tests/local/test_install.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest +from nemo_platform_ext.local.install import services_extra_install_command + + +@pytest.mark.parametrize( + ("uv_tool_env", "expected"), + [ + (False, "pip install 'nemo-platform[all]'"), + (True, "uv tool install 'nemo-platform[all]'"), + ], +) +def test_services_extra_install_command_matches_install_method( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, uv_tool_env: bool, expected: str +) -> None: + if uv_tool_env: + (tmp_path / "uv-receipt.toml").write_text("[tool]\n") + monkeypatch.setattr(sys, "prefix", str(tmp_path)) + + assert services_extra_install_command() == expected diff --git a/script/compile-wheel-constraints.sh b/script/compile-wheel-constraints.sh index 3bd03a57f5..56195f386a 100755 --- a/script/compile-wheel-constraints.sh +++ b/script/compile-wheel-constraints.sh @@ -35,7 +35,7 @@ emit_constraints() { local wheel="$1" spec="$2" label="$3" out="$4" venv meta venv="$(mktemp -d)" meta="$(mktemp -d)" - uv venv "${venv}" --python 3.11 --quiet + uv venv "${venv}" --python 3.12 --quiet # Resolve+install once with the cap so we snapshot consistent, py3.14-safe versions. printf '%s\n' "${LITELLM_CAP%% *}" >"${meta}/cap.txt" uv pip install --python "${venv}/bin/python" --constraint "${meta}/cap.txt" "${spec}" >/dev/null @@ -61,7 +61,7 @@ PY printf '# Regenerate with: script/compile-wheel-constraints.sh \n#\n' while read -r name; do [[ -n "${name}" ]] || continue - ver="$("${venv}/bin/python" -c "import importlib.metadata as m; print(m.version('${name}'))" 2>/dev/null || true) + ver="$("${venv}/bin/python" -c "import importlib.metadata as m; print(m.version('${name}'))" 2>/dev/null || true)" [[ -n "${ver}" ]] && printf '%s==%s\n' "${name}" "${ver}" done <"${meta}/names.txt" | sort printf '%s\n' "${LITELLM_CAP}" diff --git a/script/test-nemo-cli.sh b/script/test-nemo-cli.sh index cde8c02b98..558783767b 100755 --- a/script/test-nemo-cli.sh +++ b/script/test-nemo-cli.sh @@ -52,7 +52,20 @@ done echo "----- nemo on PATH -----" command -v nemo -nemo --version +version_output="$(nemo --version)" +echo "${version_output}" +# A bare `0.0.0` is the CLI's fallback when its importlib.metadata lookup +# misses the distribution it was installed from, which is invisible unless the +# output is asserted. Match it exactly: nightly wheels are stamped with a +# sentinel `0.0.0.dev` that is a legitimate version string. The +# pattern is the subset of PEP 440 this project stamps — release, optional +# pre-release, any number of .post/.dev segments, optional local version. +version="${version_output#nemo version }" +if [[ "${version}" == "0.0.0" ]] || + [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?(\.(post|dev)[0-9]+)*(\+[0-9a-z.]+)?$ ]]; then + echo "::error::nemo --version did not report an installed version: ${version_output}" >&2 + exit 1 +fi # Import-time checks for commands that ship in the bundled wrapper. We don't # call `nemo services --help` here because `nemo services run` below is a diff --git a/sdk/python/nemo-platform/pyproject.toml b/sdk/python/nemo-platform/pyproject.toml index d45b878903..482fed2314 100644 --- a/sdk/python/nemo-platform/pyproject.toml +++ b/sdk/python/nemo-platform/pyproject.toml @@ -26,13 +26,12 @@ dependencies = [ "fsspec>=2023.1.0", ] -requires-python = ">= 3.12" +requires-python = ">=3.12,<3.14" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", diff --git a/sdk/python/nemo-platform/src/nemo_platform/_version.py b/sdk/python/nemo-platform/src/nemo_platform/_version.py index a5a2651220..e7511e7393 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/_version.py +++ b/sdk/python/nemo-platform/src/nemo_platform/_version.py @@ -5,7 +5,7 @@ __title__ = "nemo_platform" try: - __version__ = _package_version("nemo-platform-sdk") + __version__ = _package_version("nemo-platform") except PackageNotFoundError: __version__ = "0.0.0" # Injected at release time for non-production builds; None for RC and production releases. diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/services/cli.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/services/cli.py index c63b6bf9db..7026222b9a 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/services/cli.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/services/cli.py @@ -15,6 +15,7 @@ import typer from nemo_platform.cli.core.help_formatter import create_typer_app from nemo_platform.cli.docker_preflight import require_docker_for_default_local +from nemo_platform.local.install import services_extra_install_command from nemo_platform.local.process import ( ForegroundInstanceError, InstanceAlreadyRunningError, @@ -75,10 +76,7 @@ def _require_services_extra() -> None: "Running local platform services needs extra components that aren't installed yet.\n" "\n" "Install them with:\n" - " pip install 'nemo-platform[all]'\n" - "\n" - "On Python 3.14, use:\n" - " PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 pip install 'nemo-platform[all]'\n", + f" {services_extra_install_command()}\n", err=True, ) raise typer.Exit(1) diff --git a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/setup.py b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/setup.py index beda9ede74..09d20f1a3b 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/cli/commands/setup.py +++ b/sdk/python/nemo-platform/src/nemo_platform/cli/commands/setup.py @@ -36,6 +36,7 @@ from pydantic import SecretStr from rich import box from rich.console import Console +from rich.markup import escape from rich.panel import Panel from nemo_platform.cli.commands.skills import registry as skills_registry @@ -49,6 +50,7 @@ from nemo_platform.client.tls import client_verify_from_env from nemo_platform.config.config import Config from nemo_platform.config.models import DEFAULT_BASE_URL, ConfigFile, ConfigParams, LocalServicesConfig, NoAuthUser +from nemo_platform.local.install import services_extra_install_command from nemo_platform.local.process import ( PortConflict, check_port_available_for_start, @@ -1154,9 +1156,7 @@ def _maybe_start_services( if importlib.util.find_spec("pyleak") is None: console.print(f"{CROSS} Local services require extra dependencies that aren't installed.") console.print(" Install them with:") - console.print(" [cyan]pip install 'nemo-platform\\[all]'[/cyan]") - console.print(" On Python 3.14, use:") - console.print(" [cyan]PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 pip install 'nemo-platform\\[all]'[/cyan]") + console.print(f" [cyan]{escape(services_extra_install_command())}[/cyan]") raise typer.Exit(1) # Fail before stop/spawn when default local needs Docker (NVBug 6537617). diff --git a/sdk/python/nemo-platform/src/nemo_platform/local/install.py b/sdk/python/nemo-platform/src/nemo_platform/local/install.py new file mode 100644 index 0000000000..52b2c7a383 --- /dev/null +++ b/sdk/python/nemo-platform/src/nemo_platform/local/install.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""How this NeMo Platform install was created.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +def services_extra_install_command() -> str: + """Return the command that adds the packaged service dependencies here. + + ``uv tool install`` environments are managed by uv and are not meant to be + edited with pip; re-running the tool install with the extra upgrades them + in place. uv marks such an environment with a ``uv-receipt.toml`` at its + root, which is what distinguishes it from an ordinary virtual environment. + """ + if (Path(sys.prefix) / "uv-receipt.toml").is_file(): + return "uv tool install 'nemo-platform[all]'" + return "pip install 'nemo-platform[all]'" diff --git a/sdk/python/nemo-platform/src/nemo_platform/local/services.py b/sdk/python/nemo-platform/src/nemo_platform/local/services.py index 9930d331d6..bbd1091f46 100644 --- a/sdk/python/nemo-platform/src/nemo_platform/local/services.py +++ b/sdk/python/nemo-platform/src/nemo_platform/local/services.py @@ -21,6 +21,7 @@ from nemo_platform import AsyncNeMoPlatform, NeMoPlatform from nemo_platform.local import process +from nemo_platform.local.install import services_extra_install_command from nemo_platform.local.transport import ( EMBEDDED_BASE_URL, UDS_BASE_URL, @@ -407,7 +408,7 @@ async def stop_async(self, *, timeout: float = 30.0, force: bool = False) -> pro def require_services_extra() -> None: if importlib.util.find_spec("pyleak") is not None: return - raise ServicesExtraRequiredError("Install service dependencies with `pip install 'nemo-platform[all]'`.") + raise ServicesExtraRequiredError(f"Install service dependencies with `{services_extra_install_command()}`.") def _validate_socket_path_length(socket_path: Path) -> None: diff --git a/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/local/test_install.py b/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/local/test_install.py new file mode 100644 index 0000000000..b529a43320 --- /dev/null +++ b/sdk/python/nemo-platform/tests/vendored/nemo_platform_ext/local/test_install.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest +from nemo_platform.local.install import services_extra_install_command + + +@pytest.mark.parametrize( + ("uv_tool_env", "expected"), + [ + (False, "pip install 'nemo-platform[all]'"), + (True, "uv tool install 'nemo-platform[all]'"), + ], +) +def test_services_extra_install_command_matches_install_method( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, uv_tool_env: bool, expected: str +) -> None: + if uv_tool_env: + (tmp_path / "uv-receipt.toml").write_text("[tool]\n") + monkeypatch.setattr(sys, "prefix", str(tmp_path)) + + assert services_extra_install_command() == expected diff --git a/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/post_generation_update.py b/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/post_generation_update.py index dfa9501f85..e707f5a1d8 100755 --- a/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/post_generation_update.py +++ b/tools/nemo-platform-sdk-tools/src/nemo_platform_sdk_tools/sdk/post_generation_update.py @@ -16,7 +16,7 @@ import tomlkit import typer -from nemo_platform_sdk_tools.sdk.core.common import SdkInfo, get_sdk_info +from nemo_platform_sdk_tools.sdk.core.common import WRAPPER_DISTRIBUTION_NAME, SdkInfo, get_sdk_info from nemo_platform_sdk_tools.sdk.post_generation_exist_ok import inject_exist_ok app = typer.Typer( @@ -24,7 +24,14 @@ ) -def sdk_version_file_content(package_name: str) -> str: +def sdk_version_file_content(distribution_name: str) -> str: + """Render ``_version.py``, which reads the version from installed metadata. + + ``distribution_name`` is the distribution to query at runtime, which is not + the distribution being built: the SDK ships bundled inside the + ``nemo-platform`` wheel, so ``nemo-platform-sdk`` metadata only exists in + the workspace. Both report the same version there. + """ return f'''# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 @@ -32,7 +39,7 @@ def sdk_version_file_content(package_name: str) -> str: __title__ = "nemo_platform" try: - __version__ = _package_version("{package_name}") + __version__ = _package_version("{distribution_name}") except PackageNotFoundError: __version__ = "0.0.0" # Injected at release time for non-production builds; None for RC and production releases. @@ -195,15 +202,15 @@ def update_pyproject_toml(sdk_info: SdkInfo) -> bool: project["urls"] = {"Homepage": "https://docs.nvidia.com/nemo/microservices/latest/about/index.html"} - # Drop support for Python versions older than 3.12. - version_to_remove = ["3.8", "3.9", "3.10", "3.11"] + # Match the supported range of the nemo-platform wheel this SDK ships in. + version_to_remove = ["3.8", "3.9", "3.10", "3.11", "3.14"] for ver in version_to_remove: try: project["classifiers"].remove(f"Programming Language :: Python :: {ver}") except ValueError: pass - project["requires-python"] = ">= 3.12" + project["requires-python"] = ">=3.12,<3.14" dependencies = project.get("dependencies") if dependencies is not None: for dep in list(dependencies): @@ -624,7 +631,7 @@ def ensure_api_image_field() -> None: version_file = sdk_info.sdk_dir / "src" / sdk_info.module_name / "_version.py" content = version_file.read_text(encoding="utf-8") - expected = sdk_version_file_content(sdk_info.package_name) + expected = sdk_version_file_content(WRAPPER_DISTRIBUTION_NAME) if content == expected: typer.echo(" - Dynamic version metadata already present, skipping.") return