From 582d1e1eed4ddaf94cf9913ca0e463229237c289 Mon Sep 17 00:00:00 2001 From: Blake Moore Date: Tue, 19 May 2026 20:24:16 +0100 Subject: [PATCH] Addresses #84 --- CHANGELOG.md | 3 ++ README.adoc | 38 +++++++++++++++++++++ README.md | 42 +++++++++++++++++++++++ domino/domino.py | 55 ++++++++++++++++++++++++++++++ domino/routes.py | 18 ++++++++++ tests/test_endpoints.py | 75 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 231 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72c25d5b..fa58c33b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to the `python-domino` library will be documented in this fi ## [Unreleased] ### Added +* `model_deployment_start(model_id, model_version_id)` — start (or restart) the serving endpoint for a deployed model version. Closes #84. +* `model_deployment_stop(model_id, model_version_id)` — stop a deployed model version. Closes #84. +* `model_deployment_status(model_id, model_version_id)` — query the deployment status of a model version (returns `running`, `stopped`, etc.). * `scripts/check_snake_case.py` — AST-based lint script that catches camelCase parameter names in new code. * GitHub Actions CI workflow (`.github/workflows/ci.yml`) that runs lint, type-checking, and tests on every PR and push to `master`. All checks must pass before a PR can be merged. * `pyproject.toml` with `isort` and `black` configuration (`profile = "black"`, `target-version = ["py310"]`). diff --git a/README.adoc b/README.adoc index 6f9f3a12..90ef1440 100644 --- a/README.adoc +++ b/README.adoc @@ -602,6 +602,44 @@ Read-only property. Returns the ID of the first app in the current project, or ` print(d.app_id) # e.g. "aabbccddeeff001122334457" ---- +=== Model deployment lifecycle + +Methods for starting, stopping, and checking the status of a deployed model API version. A "model" can have multiple versions; lifecycle is controlled per-version. + +==== model_deployment_start(model_id, model_version_id) + +Start (or restart) the serving endpoint for a specific model version. + +* _model_id (string):_ The ID of the model. +* _model_version_id (string):_ The ID of the model version to start. + +Returns the HTTP response from the Domino API. + +==== model_deployment_stop(model_id, model_version_id) + +Stop the serving endpoint for a specific model version. Useful for taking a deployed model offline (for example to save compute when the endpoint isn't in use). + +* _model_id (string):_ The ID of the model. +* _model_version_id (string):_ The ID of the model version to stop. + +Returns the HTTP response from the Domino API. + +==== model_deployment_status(model_id, model_version_id) + +Get the current deployment status of a specific model version. + +* _model_id (string):_ The ID of the model. +* _model_version_id (string):_ The ID of the model version. + +Returns a dict with `modelId`, `modelVersionId`, and `status` (e.g. `"running"`, `"stopped"`). + +[source,python] +---- +status = d.model_deployment_status("64...", "65...") +if status["status"] == "stopped": + d.model_deployment_start("64...", "65...") +---- + === Jobs NOTE: Prefer `job_start` over `runs_start` for all new work. See the <> section for a full comparison. diff --git a/README.md b/README.md index 150601f7..9ccd20f9 100644 --- a/README.md +++ b/README.md @@ -640,6 +640,48 @@ or `None` if no app exists. Useful when you need the app ID to pass to print(d.app_id) # e.g. "aabbccddeeff001122334457" ``` +## Model deployment lifecycle + +Methods for starting, stopping, and checking the status of a deployed model +API version. A "model" can have multiple versions; lifecycle is controlled +per-version. + +### model_deployment_start(model_id, model_version_id) + +Start (or restart) the serving endpoint for a specific model version. + +- *model_id (string):* The ID of the model. +- *model_version_id (string):* The ID of the model version to start. + +Returns the HTTP response from the Domino API. + +### model_deployment_stop(model_id, model_version_id) + +Stop the serving endpoint for a specific model version. Useful for taking +a deployed model offline (e.g. to save compute when the endpoint isn't +in use). + +- *model_id (string):* The ID of the model. +- *model_version_id (string):* The ID of the model version to stop. + +Returns the HTTP response from the Domino API. + +### model_deployment_status(model_id, model_version_id) + +Get the current deployment status of a specific model version. + +- *model_id (string):* The ID of the model. +- *model_version_id (string):* The ID of the model version. + +Returns a dict with `modelId`, `modelVersionId`, and `status` (e.g. +`"running"`, `"stopped"`). + +```python +status = d.model_deployment_status("64...", "65...") +if status["status"] == "stopped": + d.model_deployment_start("64...", "65...") +``` + ## Jobs > **Prefer `job_start` over `runs_start` for all new work.** See the [Executions](#executions) section for a full comparison. diff --git a/domino/domino.py b/domino/domino.py index 3ff8dce5..c41be936 100644 --- a/domino/domino.py +++ b/domino/domino.py @@ -1689,6 +1689,61 @@ def model_version_export_logs(self, model_export_id): url = self._routes.model_version_export_logs(model_export_id) return self._get(url) + def model_deployment_start(self, model_id: str, model_version_id: str): + """ + Start the deployment of a specific model API version. + + Wraps POST /v4/models/{model_id}/{model_version_id}/startModelDeployment. + Use this to bring a stopped model version's serving endpoint back online. + + :param model_id: The id of the model. + :param model_version_id: The id of the model version to start. + :return: The HTTP response from the Domino API. + """ + if not model_id: + raise ValueError("model_id is required") + if not model_version_id: + raise ValueError("model_version_id is required") + url = self._routes.model_deployment_start(model_id, model_version_id) + return self.request_manager.post(url) + + def model_deployment_stop(self, model_id: str, model_version_id: str): + """ + Stop the deployment of a specific model API version. + + Wraps POST /v4/models/{model_id}/{model_version_id}/stopModelDeployment. + Useful for taking a running model offline (e.g. to save compute when + the endpoint is not in use). + + :param model_id: The id of the model. + :param model_version_id: The id of the model version to stop. + :return: The HTTP response from the Domino API. + """ + if not model_id: + raise ValueError("model_id is required") + if not model_version_id: + raise ValueError("model_version_id is required") + url = self._routes.model_deployment_stop(model_id, model_version_id) + return self.request_manager.post(url) + + def model_deployment_status(self, model_id: str, model_version_id: str): + """ + Get the deployment status of a specific model API version. + + Wraps GET /v4/models/{model_id}/{model_version_id}/getModelDeploymentStatus. + The response includes a `status` field (e.g. "running", "stopped"). + + :param model_id: The id of the model. + :param model_version_id: The id of the model version to query. + :return: Dict with model deployment status (modelId, modelVersionId, status). + """ + if not model_id: + raise ValueError("model_id is required") + if not model_version_id: + raise ValueError("model_version_id is required") + url = self._routes.model_deployment_status(model_id, model_version_id) + return self._get(url) + # Hardware Tier Functions def hardware_tiers_list(self): url = self._routes.hardware_tiers_list(self.project_id) diff --git a/domino/routes.py b/domino/routes.py index 9b5c3f7b..ae388cef 100644 --- a/domino/routes.py +++ b/domino/routes.py @@ -163,6 +163,24 @@ def model_version_export_status(self, model_export_id): def model_version_export_logs(self, model_export_id): return self._build_models_v4_url() + "/" + model_export_id + "/getExportLogs" + def model_deployment_start(self, model_id, model_version_id): + return ( + self._build_models_v4_url() + + f"/{model_id}/{model_version_id}/startModelDeployment" + ) + + def model_deployment_stop(self, model_id, model_version_id): + return ( + self._build_models_v4_url() + + f"/{model_id}/{model_version_id}/stopModelDeployment" + ) + + def model_deployment_status(self, model_id, model_version_id): + return ( + self._build_models_v4_url() + + f"/{model_id}/{model_version_id}/getModelDeploymentStatus" + ) + # Environment URLs def _build_v4_environments_url(self) -> str: return self.host + "/v4/environments" diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index fc38d599..90ce3555 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -245,6 +245,81 @@ def test_model_version_export_logs_returns_dict(requests_mock, dummy_hostname): assert result["logs"] == "export log output" +# --------------------------------------------------------------------------- +# Model deployment lifecycle (start / stop / status) — #84 +# --------------------------------------------------------------------------- + + +@pytest.mark.usefixtures("clear_token_file_from_env", "base_mocks") +def test_model_deployment_start_posts_to_correct_url(requests_mock, dummy_hostname): + start_mock = requests_mock.post( + f"{dummy_hostname}/v4/models/{MOCK_MODEL_ID}/" + f"{MOCK_MODEL_VERSION_ID}/startModelDeployment", + status_code=200, + ) + d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") + response = d.model_deployment_start(MOCK_MODEL_ID, MOCK_MODEL_VERSION_ID) + assert start_mock.called + assert response.status_code == 200 + + +@pytest.mark.usefixtures("clear_token_file_from_env", "base_mocks") +def test_model_deployment_stop_posts_to_correct_url(requests_mock, dummy_hostname): + stop_mock = requests_mock.post( + f"{dummy_hostname}/v4/models/{MOCK_MODEL_ID}/" + f"{MOCK_MODEL_VERSION_ID}/stopModelDeployment", + status_code=200, + ) + d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") + response = d.model_deployment_stop(MOCK_MODEL_ID, MOCK_MODEL_VERSION_ID) + assert stop_mock.called + assert response.status_code == 200 + + +@pytest.mark.usefixtures("clear_token_file_from_env", "base_mocks") +def test_model_deployment_status_returns_dict(requests_mock, dummy_hostname): + requests_mock.get( + f"{dummy_hostname}/v4/models/{MOCK_MODEL_ID}/" + f"{MOCK_MODEL_VERSION_ID}/getModelDeploymentStatus", + json={ + "modelId": MOCK_MODEL_ID, + "modelVersionId": MOCK_MODEL_VERSION_ID, + "status": "running", + }, + ) + d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") + result = d.model_deployment_status(MOCK_MODEL_ID, MOCK_MODEL_VERSION_ID) + assert result["status"] == "running" + assert result["modelId"] == MOCK_MODEL_ID + assert result["modelVersionId"] == MOCK_MODEL_VERSION_ID + + +@pytest.mark.parametrize( + "method_name", + ["model_deployment_start", "model_deployment_stop", "model_deployment_status"], +) +@pytest.mark.usefixtures("clear_token_file_from_env", "base_mocks") +def test_model_deployment_methods_require_model_id( + requests_mock, dummy_hostname, method_name +): + d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") + with pytest.raises(ValueError, match="model_id is required"): + getattr(d, method_name)("", MOCK_MODEL_VERSION_ID) + + +@pytest.mark.parametrize( + "method_name", + ["model_deployment_start", "model_deployment_stop", "model_deployment_status"], +) +@pytest.mark.usefixtures("clear_token_file_from_env", "base_mocks") +def test_model_deployment_methods_require_model_version_id( + requests_mock, dummy_hostname, method_name +): + d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") + with pytest.raises(ValueError, match="model_version_id is required"): + getattr(d, method_name)(MOCK_MODEL_ID, "") + + # --------------------------------------------------------------------------- # Integration tests (require a live Domino deployment) # ---------------------------------------------------------------------------