From 1ab6264e7fbc26cd688d9107bfa2c0f29a9e13f6 Mon Sep 17 00:00:00 2001 From: Blake Moore Date: Tue, 21 Apr 2026 14:47:05 +0100 Subject: [PATCH 1/9] rename camelCase parameters to snake_case with deprecation shims --- tests/test_app.py | 64 +++++++++++------------------------------------ 1 file changed, 15 insertions(+), 49 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 76c97886..f71af1a5 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -10,14 +10,14 @@ @pytest.fixture def mock_app_publish_setup(requests_mock, dummy_hostname): """ - Mocks all API calls that app_publish() depends on when an appId is provided. + Mocks all API calls that app_publish() depends on when an app_id is provided. If any dependent calls are added to app_publish or app_unpublish, they must be mocked here as well. """ requests_mock.get(f"{dummy_hostname}/version", json={"version": "9.9.9"}) - # Mock app status lookup (GET) used by app_unpublish via app_get_status + # Mock app status lookup (GET) used by app_unpublish via __app_get_status requests_mock.get( f"{dummy_hostname}/v4/modelproducts/{MOCK_APP_ID}", json={"id": MOCK_APP_ID, "status": "Running"}, @@ -44,7 +44,7 @@ def test_app_publish_with_branch(requests_mock, dummy_hostname): in the app start request body. """ d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") - d.app_publish(appId=MOCK_APP_ID, branch="my-feature-branch") + d.app_publish(app_id=MOCK_APP_ID, branch="my-feature-branch") app_start_request = next( req @@ -60,11 +60,11 @@ def test_app_publish_with_branch(requests_mock, dummy_hostname): @pytest.mark.usefixtures("clear_token_file_from_env", "mock_app_publish_setup") def test_app_publish_with_commit_id(requests_mock, dummy_hostname): """ - Confirm that the commitId parameter is correctly formatted as mainRepoGitRef + Confirm that the commit_id parameter is correctly formatted as mainRepoGitRef in the app start request body. """ d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") - d.app_publish(appId=MOCK_APP_ID, commitId="abc123def456") + d.app_publish(app_id=MOCK_APP_ID, commit_id="abc123def456") app_start_request = next( req @@ -81,10 +81,10 @@ def test_app_publish_with_commit_id(requests_mock, dummy_hostname): def test_app_publish_omits_git_ref_when_not_provided(requests_mock, dummy_hostname): """ Confirm that mainRepoGitRef is omitted from the request body when neither - branch nor commitId is provided. + branch nor commit_id is provided. """ d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") - d.app_publish(appId=MOCK_APP_ID) + d.app_publish(app_id=MOCK_APP_ID) app_start_request = next( req @@ -97,21 +97,21 @@ def test_app_publish_omits_git_ref_when_not_provided(requests_mock, dummy_hostna @pytest.mark.usefixtures("clear_token_file_from_env", "mock_app_publish_setup") def test_app_publish_raises_if_both_branch_and_commit_id_provided(dummy_hostname): """ - Confirm that providing both branch and commitId raises a ValueError. + Confirm that providing both branch and commit_id raises a ValueError. """ d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") with pytest.raises(ValueError, match="Only one of commit_id or branch"): - d.app_publish(appId=MOCK_APP_ID, branch="my-branch", commitId="abc123") + d.app_publish(app_id=MOCK_APP_ID, branch="my-branch", commit_id="abc123") @pytest.mark.usefixtures("clear_token_file_from_env", "mock_app_publish_setup") def test_app_publish_unpublishes_running_app(requests_mock, dummy_hostname): """ Confirm that app_publish calls stop on the running app before starting - when unpublishRunningApps=True (the default). + when unpublish_running_apps=True (the default). """ d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") - d.app_publish(appId=MOCK_APP_ID, unpublishRunningApps=True) + d.app_publish(app_id=MOCK_APP_ID, unpublish_running_apps=True) stop_requests = [ req @@ -124,10 +124,10 @@ def test_app_publish_unpublishes_running_app(requests_mock, dummy_hostname): @pytest.mark.usefixtures("clear_token_file_from_env", "mock_app_publish_setup") def test_app_publish_skips_unpublish_when_disabled(requests_mock, dummy_hostname): """ - Confirm that app_publish does not call stop when unpublishRunningApps=False. + Confirm that app_publish does not call stop when unpublish_running_apps=False. """ d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") - d.app_publish(appId=MOCK_APP_ID, unpublishRunningApps=False) + d.app_publish(app_id=MOCK_APP_ID, unpublish_running_apps=False) stop_requests = [ req @@ -140,11 +140,11 @@ def test_app_publish_skips_unpublish_when_disabled(requests_mock, dummy_hostname @pytest.mark.usefixtures("clear_token_file_from_env", "mock_app_publish_setup") def test_app_publish_targets_specific_app_id(requests_mock, dummy_hostname): """ - Confirm that the provided appId is used in the start endpoint URL, not the + Confirm that the provided app_id is used in the start endpoint URL, not the default first-app lookup. """ d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") - d.app_publish(appId=MOCK_APP_ID) + d.app_publish(app_id=MOCK_APP_ID) start_requests = [ req @@ -152,37 +152,3 @@ def test_app_publish_targets_specific_app_id(requests_mock, dummy_hostname): if req.path == f"/v4/modelproducts/{MOCK_APP_ID}/start" ] assert len(start_requests) == 1 - - -@pytest.mark.usefixtures("clear_token_file_from_env") -def test_app_get_status_returns_status(requests_mock, dummy_hostname): - """ - Confirm that app_get_status() is publicly accessible and returns the - status string from the API response. - """ - requests_mock.get(f"{dummy_hostname}/version", json={"version": "9.9.9"}) - requests_mock.get( - f"{dummy_hostname}/v4/modelproducts/{MOCK_APP_ID}", - json={"id": MOCK_APP_ID, "status": "Running"}, - ) - - d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") - status = d.app_get_status(MOCK_APP_ID) - assert status == "Running" - - -@pytest.mark.usefixtures("clear_token_file_from_env") -def test_app_get_status_returns_none_when_status_missing(requests_mock, dummy_hostname): - """ - Confirm that app_get_status() returns None when the API response - does not contain a status field. - """ - requests_mock.get(f"{dummy_hostname}/version", json={"version": "9.9.9"}) - requests_mock.get( - f"{dummy_hostname}/v4/modelproducts/{MOCK_APP_ID}", - json={"id": MOCK_APP_ID}, - ) - - d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") - status = d.app_get_status(MOCK_APP_ID) - assert status is None From d0b34ee318e7a04fba7f0fab5508e4076623fb73 Mon Sep 17 00:00:00 2001 From: Blake Moore Date: Tue, 21 Apr 2026 15:58:30 +0100 Subject: [PATCH 2/9] add deprecation warning tests for renamed camelCase parameters --- domino/domino.py | 1 + tests/test_deprecations.py | 215 +++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 tests/test_deprecations.py diff --git a/domino/domino.py b/domino/domino.py index ff918be6..206da12b 100644 --- a/domino/domino.py +++ b/domino/domino.py @@ -38,6 +38,7 @@ from domino.routes import _Routes + class Domino: def __init__( self, diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py new file mode 100644 index 00000000..b3acf8b6 --- /dev/null +++ b/tests/test_deprecations.py @@ -0,0 +1,215 @@ +""" +Tests confirming that renamed camelCase parameters emit DeprecationWarning +and that the call still succeeds with the old name. +""" +import pytest + +from domino import Domino + +MOCK_PROJECT_ID = "aabbccddeeff001122334454" +MOCK_RUN_ID = "aabbccddeeff001122334455" +MOCK_APP_ID = "aabbccddeeff001122334457" +MOCK_COMMIT_ID = "aabbcc112233" + + +@pytest.fixture +def client(requests_mock, dummy_hostname): + requests_mock.get(f"{dummy_hostname}/version", json={"version": "9.9.9"}) + return Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever") + + +# --------------------------------------------------------------------------- +# runs_start +# --------------------------------------------------------------------------- + +class TestRunsStartDeprecations: + @pytest.fixture(autouse=True) + def mock_runs_start(self, requests_mock, dummy_hostname): + requests_mock.post( + f"{dummy_hostname}/v1/projects/anyuser/anyproject/runs", + json={"runId": MOCK_RUN_ID}, + ) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_isDirect_warns(self, client): + with pytest.warns(DeprecationWarning, match="isDirect is deprecated"): + client.runs_start("main.py", isDirect=True) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_commitId_warns(self, client): + with pytest.warns(DeprecationWarning, match="commitId is deprecated"): + client.runs_start("main.py", commitId=MOCK_COMMIT_ID) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_publishApiEndpoint_warns(self, client): + with pytest.warns(DeprecationWarning, match="publishApiEndpoint is deprecated"): + client.runs_start("main.py", publishApiEndpoint=True) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_new_names_accepted_without_warning(self, client, recwarn): + client.runs_start("main.py", is_direct=True, commit_id=MOCK_COMMIT_ID) + deprecation_warnings = [w for w in recwarn.list if issubclass(w.category, DeprecationWarning) + and "deprecated" in str(w.message).lower() + and any(x in str(w.message) for x in ["isDirect", "commitId"])] + assert len(deprecation_warnings) == 0 + + +# --------------------------------------------------------------------------- +# runs_start_blocking (spot-check one param — same shim logic) +# --------------------------------------------------------------------------- + +class TestRunsStartBlockingDeprecations: + @pytest.fixture(autouse=True) + def mock_endpoints(self, requests_mock, dummy_hostname): + requests_mock.post( + f"{dummy_hostname}/v1/projects/anyuser/anyproject/runs", + json={"runId": MOCK_RUN_ID, "outputCommitId": MOCK_COMMIT_ID, "status": "Succeeded"}, + ) + requests_mock.get( + f"{dummy_hostname}/v1/projects/anyuser/anyproject/runs", + json={"data": [{"id": MOCK_RUN_ID, "outputCommitId": MOCK_COMMIT_ID, "status": "Succeeded"}]}, + ) + requests_mock.get( + f"{dummy_hostname}/v1/projects/anyuser/anyproject/run/{MOCK_RUN_ID}/stdout", + json={"setup": "", "stdout": "done"}, + ) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_isDirect_warns(self, client): + with pytest.warns(DeprecationWarning, match="isDirect is deprecated"): + client.runs_start_blocking("main.py", isDirect=True, poll_freq=1, max_poll_time=5) + + +# --------------------------------------------------------------------------- +# run_stop / runs_status / get_run_log / runs_stdout +# --------------------------------------------------------------------------- + +class TestLegacyRunMethodDeprecations: + @pytest.fixture(autouse=True) + def mock_run_endpoints(self, requests_mock, dummy_hostname): + requests_mock.get( + f"{dummy_hostname}/v4/gateway/projects/findProjectByOwnerAndName" + "?ownerName=anyuser&projectName=anyproject", + json={"id": MOCK_PROJECT_ID}, + ) + requests_mock.post(f"{dummy_hostname}/v4/jobs/stop", json={}) + requests_mock.get( + f"{dummy_hostname}/v1/projects/anyuser/anyproject/runs/{MOCK_RUN_ID}", + json={"id": MOCK_RUN_ID, "status": "Succeeded"}, + ) + requests_mock.get( + f"{dummy_hostname}/v1/projects/anyuser/anyproject/run/{MOCK_RUN_ID}/stdout", + json={"setup": "setup log", "stdout": "hello"}, + ) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_run_stop_runId_warns(self, client): + with pytest.warns(DeprecationWarning, match="runId is deprecated"): + client.run_stop(runId=MOCK_RUN_ID) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_run_stop_saveChanges_warns(self, client): + with pytest.warns(DeprecationWarning, match="saveChanges is deprecated"): + client.run_stop(run_id=MOCK_RUN_ID, saveChanges=False) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_runs_status_runId_warns(self, client): + with pytest.warns(DeprecationWarning, match="runId is deprecated"): + client.runs_status(runId=MOCK_RUN_ID) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_get_run_log_runId_warns(self, client): + with pytest.warns(DeprecationWarning, match="runId is deprecated"): + client.get_run_log(runId=MOCK_RUN_ID) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_get_run_log_includeSetupLog_warns(self, client): + with pytest.warns(DeprecationWarning, match="includeSetupLog is deprecated"): + client.get_run_log(run_id=MOCK_RUN_ID, includeSetupLog=False) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_runs_stdout_runId_warns(self, client): + with pytest.warns(DeprecationWarning, match="runId is deprecated"): + client.runs_stdout(runId=MOCK_RUN_ID) + + +# --------------------------------------------------------------------------- +# files_list +# --------------------------------------------------------------------------- + +class TestFilesListDeprecations: + @pytest.fixture(autouse=True) + def mock_files_list(self, requests_mock, dummy_hostname): + requests_mock.get( + f"{dummy_hostname}/v1/projects/anyuser/anyproject/files/{MOCK_COMMIT_ID}//", + json={"data": []}, + ) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_commitId_warns(self, client): + with pytest.warns(DeprecationWarning, match="commitId is deprecated"): + client.files_list(commitId=MOCK_COMMIT_ID) + + +# --------------------------------------------------------------------------- +# endpoint_publish +# --------------------------------------------------------------------------- + +class TestEndpointPublishDeprecations: + @pytest.fixture(autouse=True) + def mock_endpoint_publish(self, requests_mock, dummy_hostname): + requests_mock.post( + f"{dummy_hostname}/v1/anyuser/anyproject/endpoint/publishRelease", + json={}, + ) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_commitId_warns(self, client): + with pytest.warns(DeprecationWarning, match="commitId is deprecated"): + client.endpoint_publish("app.py", "predict", commitId=MOCK_COMMIT_ID) + + +# --------------------------------------------------------------------------- +# app_publish / app_unpublish +# --------------------------------------------------------------------------- + +class TestAppDeprecations: + @pytest.fixture(autouse=True) + def mock_app_endpoints(self, requests_mock, dummy_hostname): + requests_mock.get( + f"{dummy_hostname}/v4/modelproducts/{MOCK_APP_ID}", + json={"id": MOCK_APP_ID, "status": "Stopped"}, + ) + requests_mock.post( + f"{dummy_hostname}/v4/modelproducts/{MOCK_APP_ID}/stop", + json={}, + ) + requests_mock.post( + f"{dummy_hostname}/v4/modelproducts/{MOCK_APP_ID}/start", + json={"id": MOCK_APP_ID, "status": "Running"}, + ) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_app_publish_appId_warns(self, client): + with pytest.warns(DeprecationWarning, match="appId is deprecated"): + client.app_publish(appId=MOCK_APP_ID) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_app_publish_unpublishRunningApps_warns(self, client): + with pytest.warns(DeprecationWarning, match="unpublishRunningApps is deprecated"): + client.app_publish(app_id=MOCK_APP_ID, unpublishRunningApps=True) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_app_publish_commitId_warns(self, client): + with pytest.warns(DeprecationWarning, match="commitId is deprecated"): + client.app_publish(app_id=MOCK_APP_ID, commitId=MOCK_COMMIT_ID) + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_app_publish_environmentId_warns(self, client): + with pytest.warns(DeprecationWarning, match="environmentId is deprecated"): + client.app_publish(app_id=MOCK_APP_ID, environmentId="env-123") + + @pytest.mark.usefixtures("clear_token_file_from_env") + def test_app_unpublish_appId_warns(self, client): + with pytest.warns(DeprecationWarning, match="appId is deprecated"): + client.app_unpublish(appId=MOCK_APP_ID) From 8c69d35092e1f06d578c58e56ae0ea46a587d989 Mon Sep 17 00:00:00 2001 From: Blake Moore Date: Tue, 21 Apr 2026 16:06:31 +0100 Subject: [PATCH 3/9] Updated CHANGELOG --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 998849e6..30ea1eaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,32 @@ All notable changes to the `python-domino` library will be documented in this fi * 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"]`). +### Deprecated +The following public API parameters have been renamed to follow PEP 8 (`snake_case`). +The old names continue to work but will emit a `DeprecationWarning`. They will be +removed in the next major version. + +| Method | Old name | New name | +|--------|----------|----------| +| `runs_start`, `runs_start_blocking` | `isDirect` | `is_direct` | +| `runs_start`, `runs_start_blocking` | `commitId` | `commit_id` | +| `runs_start`, `runs_start_blocking` | `publishApiEndpoint` | `publish_api_endpoint` | +| `run_stop` | `runId` | `run_id` | +| `run_stop` | `saveChanges` | `save_changes` | +| `runs_status` | `runId` | `run_id` | +| `get_run_log` | `runId` | `run_id` | +| `get_run_log` | `includeSetupLog` | `include_setup_log` | +| `runs_stdout` | `runId` | `run_id` | +| `files_list` | `commitId` | `commit_id` | +| `endpoint_publish` | `commitId` | `commit_id` | +| `app_publish` | `unpublishRunningApps` | `unpublish_running_apps` | +| `app_publish` | `hardwareTierId` | `hardware_tier_id` | +| `app_publish` | `environmentId` | `environment_id` | +| `app_publish` | `externalVolumeMountIds` | `external_volume_mount_ids` | +| `app_publish` | `commitId` | `commit_id` | +| `app_publish` | `appId` | `app_id` | +| `app_unpublish` | `appId` | `app_id` | + ### Changed * Resolved all 38 pre-existing `mypy` type errors across `domino/`, bringing the codebase to a clean `mypy` pass with `--python-version=3.10`. * Resolved all `flake8`, `isort`, and `black` formatting errors across the codebase. From 6007a005eebdaee03e2b841628788acb6f215f69 Mon Sep 17 00:00:00 2001 From: Blake Moore Date: Wed, 22 Apr 2026 23:17:13 +0100 Subject: [PATCH 4/9] reapply mypy fixes and restore snake_case CI check --- .github/workflows/ci.yml | 6 ++++++ tests/test_deprecations.py | 41 +++++++++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d923003..ebc05acb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,12 @@ jobs: - name: flake8 run: flake8 . + - name: snake_case + run: | + find domino -name "*.py" \ + | grep -v "domino/_impl/" \ + | grep -v "domino/airflow/" \ + | xargs python scripts/check_snake_case.py typecheck: name: Type check diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index b3acf8b6..e6306a4c 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -2,6 +2,7 @@ Tests confirming that renamed camelCase parameters emit DeprecationWarning and that the call still succeeds with the old name. """ + import pytest from domino import Domino @@ -22,6 +23,7 @@ def client(requests_mock, dummy_hostname): # runs_start # --------------------------------------------------------------------------- + class TestRunsStartDeprecations: @pytest.fixture(autouse=True) def mock_runs_start(self, requests_mock, dummy_hostname): @@ -48,9 +50,13 @@ def test_publishApiEndpoint_warns(self, client): @pytest.mark.usefixtures("clear_token_file_from_env") def test_new_names_accepted_without_warning(self, client, recwarn): client.runs_start("main.py", is_direct=True, commit_id=MOCK_COMMIT_ID) - deprecation_warnings = [w for w in recwarn.list if issubclass(w.category, DeprecationWarning) - and "deprecated" in str(w.message).lower() - and any(x in str(w.message) for x in ["isDirect", "commitId"])] + deprecation_warnings = [ + w + for w in recwarn.list + if issubclass(w.category, DeprecationWarning) + and "deprecated" in str(w.message).lower() + and any(x in str(w.message) for x in ["isDirect", "commitId"]) + ] assert len(deprecation_warnings) == 0 @@ -58,16 +64,29 @@ def test_new_names_accepted_without_warning(self, client, recwarn): # runs_start_blocking (spot-check one param — same shim logic) # --------------------------------------------------------------------------- + class TestRunsStartBlockingDeprecations: @pytest.fixture(autouse=True) def mock_endpoints(self, requests_mock, dummy_hostname): requests_mock.post( f"{dummy_hostname}/v1/projects/anyuser/anyproject/runs", - json={"runId": MOCK_RUN_ID, "outputCommitId": MOCK_COMMIT_ID, "status": "Succeeded"}, + json={ + "runId": MOCK_RUN_ID, + "outputCommitId": MOCK_COMMIT_ID, + "status": "Succeeded", + }, ) requests_mock.get( f"{dummy_hostname}/v1/projects/anyuser/anyproject/runs", - json={"data": [{"id": MOCK_RUN_ID, "outputCommitId": MOCK_COMMIT_ID, "status": "Succeeded"}]}, + json={ + "data": [ + { + "id": MOCK_RUN_ID, + "outputCommitId": MOCK_COMMIT_ID, + "status": "Succeeded", + } + ] + }, ) requests_mock.get( f"{dummy_hostname}/v1/projects/anyuser/anyproject/run/{MOCK_RUN_ID}/stdout", @@ -77,13 +96,16 @@ def mock_endpoints(self, requests_mock, dummy_hostname): @pytest.mark.usefixtures("clear_token_file_from_env") def test_isDirect_warns(self, client): with pytest.warns(DeprecationWarning, match="isDirect is deprecated"): - client.runs_start_blocking("main.py", isDirect=True, poll_freq=1, max_poll_time=5) + client.runs_start_blocking( + "main.py", isDirect=True, poll_freq=1, max_poll_time=5 + ) # --------------------------------------------------------------------------- # run_stop / runs_status / get_run_log / runs_stdout # --------------------------------------------------------------------------- + class TestLegacyRunMethodDeprecations: @pytest.fixture(autouse=True) def mock_run_endpoints(self, requests_mock, dummy_hostname): @@ -137,6 +159,7 @@ def test_runs_stdout_runId_warns(self, client): # files_list # --------------------------------------------------------------------------- + class TestFilesListDeprecations: @pytest.fixture(autouse=True) def mock_files_list(self, requests_mock, dummy_hostname): @@ -155,6 +178,7 @@ def test_commitId_warns(self, client): # endpoint_publish # --------------------------------------------------------------------------- + class TestEndpointPublishDeprecations: @pytest.fixture(autouse=True) def mock_endpoint_publish(self, requests_mock, dummy_hostname): @@ -173,6 +197,7 @@ def test_commitId_warns(self, client): # app_publish / app_unpublish # --------------------------------------------------------------------------- + class TestAppDeprecations: @pytest.fixture(autouse=True) def mock_app_endpoints(self, requests_mock, dummy_hostname): @@ -196,7 +221,9 @@ def test_app_publish_appId_warns(self, client): @pytest.mark.usefixtures("clear_token_file_from_env") def test_app_publish_unpublishRunningApps_warns(self, client): - with pytest.warns(DeprecationWarning, match="unpublishRunningApps is deprecated"): + with pytest.warns( + DeprecationWarning, match="unpublishRunningApps is deprecated" + ): client.app_publish(app_id=MOCK_APP_ID, unpublishRunningApps=True) @pytest.mark.usefixtures("clear_token_file_from_env") From 730fb4e63bdda0eb4cc7646d15c49dd4fb794950 Mon Sep 17 00:00:00 2001 From: Blake Moore Date: Tue, 12 May 2026 18:20:30 +0100 Subject: [PATCH 5/9] raise ValueError when both deprecated and new param names are passed --- domino/domino.py | 281 +++++++++++++++++-------------------- tests/test_deprecations.py | 46 ++++++ 2 files changed, 173 insertions(+), 154 deletions(-) diff --git a/domino/domino.py b/domino/domino.py index 206da12b..5be2b118 100644 --- a/domino/domino.py +++ b/domino/domino.py @@ -37,6 +37,35 @@ from domino.http_request_manager import _HttpRequestManager from domino.routes import _Routes +# Sentinel used as the default value of renamed parameters so we can detect +# whether the caller passed the new name, the deprecated name, or neither. +_UNSET: Any = object() + + +def _resolve_renamed_kwarg(new_value, old_name, new_name, kwargs, default): + """Resolve a renamed parameter, raising if both old and new names were passed. + + If the deprecated name is present in kwargs, emit a DeprecationWarning and + return its value. If both the new name (non-sentinel) and the deprecated + name are provided in the same call, raise ValueError so the caller can fix + the ambiguity instead of silently picking one. + """ + has_old = old_name in kwargs + has_new = new_value is not _UNSET + if has_old and has_new: + raise ValueError( + f"Pass either '{new_name}' or '{old_name}', not both. " + f"'{old_name}' is deprecated; use '{new_name}'." + ) + if has_old: + warnings.warn( + f"{old_name} is deprecated, use {new_name}", + DeprecationWarning, + stacklevel=3, + ) + return kwargs.pop(old_name) + return new_value if has_new else default + class Domino: @@ -130,38 +159,30 @@ def runs_list(self): def runs_start( self, command, - is_direct=False, - commit_id=None, + is_direct=_UNSET, + commit_id=_UNSET, title=None, tier=None, - publish_api_endpoint=None, + publish_api_endpoint=_UNSET, **kwargs, ): """ Start a run via the legacy v1 Runs API. For new work, prefer job_start() which uses the v4 Jobs API and supports compute clusters, external volumes, and branch targeting. """ - if "isDirect" in kwargs: - warnings.warn( - "isDirect is deprecated, use is_direct", - DeprecationWarning, - stacklevel=2, - ) - is_direct = kwargs.pop("isDirect") - if "commitId" in kwargs: - warnings.warn( - "commitId is deprecated, use commit_id", - DeprecationWarning, - stacklevel=2, - ) - commit_id = kwargs.pop("commitId") - if "publishApiEndpoint" in kwargs: - warnings.warn( - "publishApiEndpoint is deprecated, use publish_api_endpoint", - DeprecationWarning, - stacklevel=2, - ) - publish_api_endpoint = kwargs.pop("publishApiEndpoint") + is_direct = _resolve_renamed_kwarg( + is_direct, "isDirect", "is_direct", kwargs, False + ) + commit_id = _resolve_renamed_kwarg( + commit_id, "commitId", "commit_id", kwargs, None + ) + publish_api_endpoint = _resolve_renamed_kwarg( + publish_api_endpoint, + "publishApiEndpoint", + "publish_api_endpoint", + kwargs, + None, + ) url = self._routes.runs_start() @@ -185,11 +206,11 @@ def runs_start( def runs_start_blocking( self, command, - is_direct=False, - commit_id=None, + is_direct=_UNSET, + commit_id=_UNSET, title=None, tier=None, - publish_api_endpoint=None, + publish_api_endpoint=_UNSET, poll_freq=5, max_poll_time=6000, retry_count=5, @@ -240,27 +261,19 @@ def runs_start_blocking( (in-case of transient http errors). If this threshold exceeds, an exception is raised. """ - if "isDirect" in kwargs: - warnings.warn( - "isDirect is deprecated, use is_direct", - DeprecationWarning, - stacklevel=2, - ) - is_direct = kwargs.pop("isDirect") - if "commitId" in kwargs: - warnings.warn( - "commitId is deprecated, use commit_id", - DeprecationWarning, - stacklevel=2, - ) - commit_id = kwargs.pop("commitId") - if "publishApiEndpoint" in kwargs: - warnings.warn( - "publishApiEndpoint is deprecated, use publish_api_endpoint", - DeprecationWarning, - stacklevel=2, - ) - publish_api_endpoint = kwargs.pop("publishApiEndpoint") + is_direct = _resolve_renamed_kwarg( + is_direct, "isDirect", "is_direct", kwargs, False + ) + commit_id = _resolve_renamed_kwarg( + commit_id, "commitId", "commit_id", kwargs, None + ) + publish_api_endpoint = _resolve_renamed_kwarg( + publish_api_endpoint, + "publishApiEndpoint", + "publish_api_endpoint", + kwargs, + None, + ) run_response = self.runs_start( command, is_direct, commit_id, title, tier, publish_api_endpoint @@ -318,32 +331,20 @@ def runs_start_blocking( return run_response - def run_stop(self, run_id=None, save_changes=True, **kwargs): - if "runId" in kwargs: - warnings.warn( - "runId is deprecated, use run_id", DeprecationWarning, stacklevel=2 - ) - run_id = kwargs.pop("runId") - if "saveChanges" in kwargs: - warnings.warn( - "saveChanges is deprecated, use save_changes", - DeprecationWarning, - stacklevel=2, - ) - save_changes = kwargs.pop("saveChanges") + def run_stop(self, run_id=_UNSET, save_changes=_UNSET, **kwargs): + run_id = _resolve_renamed_kwarg(run_id, "runId", "run_id", kwargs, None) + save_changes = _resolve_renamed_kwarg( + save_changes, "saveChanges", "save_changes", kwargs, True + ) self.log.warning("Use job_stop method instead") return self.job_stop(job_id=run_id, commit_results=save_changes) - def runs_status(self, run_id=None, **kwargs): - if "runId" in kwargs: - warnings.warn( - "runId is deprecated, use run_id", DeprecationWarning, stacklevel=2 - ) - run_id = kwargs.pop("runId") + def runs_status(self, run_id=_UNSET, **kwargs): + run_id = _resolve_renamed_kwarg(run_id, "runId", "run_id", kwargs, None) url = self._routes.runs_status(run_id) return self._get(url) - def get_run_log(self, run_id=None, include_setup_log=True, **kwargs): + def get_run_log(self, run_id=_UNSET, include_setup_log=_UNSET, **kwargs): """ Get the unified log for a run (setup + stdout). @@ -354,18 +355,14 @@ def get_run_log(self, run_id=None, include_setup_log=True, **kwargs): include_setup_log : bool whether or not to include the setup log in the output. """ - if "runId" in kwargs: - warnings.warn( - "runId is deprecated, use run_id", DeprecationWarning, stacklevel=2 - ) - run_id = kwargs.pop("runId") - if "includeSetupLog" in kwargs: - warnings.warn( - "includeSetupLog is deprecated, use include_setup_log", - DeprecationWarning, - stacklevel=2, - ) - include_setup_log = kwargs.pop("includeSetupLog") + run_id = _resolve_renamed_kwarg(run_id, "runId", "run_id", kwargs, None) + include_setup_log = _resolve_renamed_kwarg( + include_setup_log, + "includeSetupLog", + "include_setup_log", + kwargs, + True, + ) url = self._routes.runs_stdout(run_id) @@ -383,7 +380,7 @@ def get_run_info(self, run_id): if run_info["id"] == run_id: return run_info - def runs_stdout(self, run_id=None, **kwargs): + def runs_stdout(self, run_id=_UNSET, **kwargs): """ Get std out emitted by a particular run. @@ -392,11 +389,7 @@ def runs_stdout(self, run_id=None, **kwargs): run_id : string the id associated with the run. """ - if "runId" in kwargs: - warnings.warn( - "runId is deprecated, use run_id", DeprecationWarning, stacklevel=2 - ) - run_id = kwargs.pop("runId") + run_id = _resolve_renamed_kwarg(run_id, "runId", "run_id", kwargs, None) html_start_tags = ( "
 bool:
+    return bool(INCLUDE_RE.match(path)) and not EXCLUDE_RE.match(path)
+
 
 def check_file(path: str) -> list[tuple[int, str]]:
     violations = []
@@ -28,6 +42,8 @@ def check_file(path: str) -> list[tuple[int, str]]:
     files = sys.argv[1:] or []
     found = False
     for path in files:
+        if not should_check(path):
+            continue
         for lineno, name in check_file(path):
             print(f"{path}:{lineno}: camelCase parameter '{name}'")
             found = True

From d219a43594f885b36ffd4ef8e01141276646e086 Mon Sep 17 00:00:00 2001
From: Blake Moore 
Date: Tue, 12 May 2026 18:25:43 +0100
Subject: [PATCH 7/9] =?UTF-8?q?clarify=20that=20camelCase=20=E2=86=92=20sn?=
 =?UTF-8?q?ake=5Fcase=20renames=20are=20Python-side=20only?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md | 4 ++++
 README.adoc  | 2 +-
 README.md    | 5 +++++
 3 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30ea1eaf..72c25d5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ The following public API parameters have been renamed to follow PEP 8 (`snake_ca
 The old names continue to work but will emit a `DeprecationWarning`. They will be
 removed in the next major version.
 
+> **Note:** these renames are Python-side only. The JSON keys sent to the
+> Domino HTTP API are unchanged — the SDK still emits `commitId`, `isDirect`,
+> `hardwareTierId`, etc. on the wire. The HTTP API contract is not affected.
+
 | Method | Old name | New name |
 |--------|----------|----------|
 | `runs_start`, `runs_start_blocking` | `isDirect` | `is_direct` |
diff --git a/README.adoc b/README.adoc
index b8f06ce1..6f9f3a12 100644
--- a/README.adoc
+++ b/README.adoc
@@ -577,7 +577,7 @@ d.app_publish(commit_id="abc123def456")
 d.app_publish(app_id="aabbccddeeff001122334457")
 ----
 
-NOTE: The parameters `unpublishRunningApps`, `hardwareTierId`, `environmentId`, `externalVolumeMountIds`, `commitId`, and `appId` are deprecated and will be removed in the next major version. Use the `snake_case` equivalents listed above.
+NOTE: The parameters `unpublishRunningApps`, `hardwareTierId`, `environmentId`, `externalVolumeMountIds`, `commitId`, and `appId` are deprecated and will be removed in the next major version. Use the `snake_case` equivalents listed above. These renames are Python-side only — the JSON keys sent to the Domino HTTP API are unchanged (the SDK still emits `commitId`, `hardwareTierId`, etc. on the wire). The HTTP API contract is not affected.
 
 ==== app_unpublish(app_id=None)
 
diff --git a/README.md b/README.md
index 42051fe2..150601f7 100644
--- a/README.md
+++ b/README.md
@@ -608,6 +608,11 @@ d.app_publish(app_id="aabbccddeeff001122334457")
 > `environmentId`, `externalVolumeMountIds`, `commitId`, and `appId`
 > are deprecated and will be removed in the next major version.
 > Use the `snake_case` equivalents listed above.
+>
+> These renames are Python-side only — the JSON keys sent to the
+> Domino HTTP API are unchanged (the SDK still emits `commitId`,
+> `hardwareTierId`, etc. on the wire). The HTTP API contract is
+> not affected.
 
 ### app_unpublish(app_id=None)
 

From 86c89c56305e87ce3573f363abd6cd739f031fd0 Mon Sep 17 00:00:00 2001
From: Blake Moore 
Date: Tue, 19 May 2026 13:56:08 +0100
Subject: [PATCH 8/9] fix formatting introduced during rebase conflict
 resolution

The conflict resolution for commit b45ea77 left an extra blank line
between the _resolve_renamed_kwarg helper and the Domino class.
Black removed it. No semantic change.

Co-Authored-By: Claude Opus 4.7 (1M context) 
---
 domino/domino.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/domino/domino.py b/domino/domino.py
index 5be2b118..d6877c33 100644
--- a/domino/domino.py
+++ b/domino/domino.py
@@ -67,7 +67,6 @@ def _resolve_renamed_kwarg(new_value, old_name, new_name, kwargs, default):
     return new_value if has_new else default
 
 
-
 class Domino:
     def __init__(
         self,

From 9a24604dfc11c8c1c5349f272a90b8bf1a964a48 Mon Sep 17 00:00:00 2001
From: Blake Moore 
Date: Tue, 19 May 2026 13:59:49 +0100
Subject: [PATCH 9/9] require run_id on run_stop to restore pre-rename safety

---
 domino/domino.py   |  2 ++
 tests/test_jobs.py | 13 +++++++++++++
 2 files changed, 15 insertions(+)

diff --git a/domino/domino.py b/domino/domino.py
index d6877c33..3ff8dce5 100644
--- a/domino/domino.py
+++ b/domino/domino.py
@@ -335,6 +335,8 @@ def run_stop(self, run_id=_UNSET, save_changes=_UNSET, **kwargs):
         save_changes = _resolve_renamed_kwarg(
             save_changes, "saveChanges", "save_changes", kwargs, True
         )
+        if not run_id:
+            raise ValueError("run_id is required")
         self.log.warning("Use job_stop method instead")
         return self.job_stop(job_id=run_id, commit_results=save_changes)
 
diff --git a/tests/test_jobs.py b/tests/test_jobs.py
index 7816f4b2..123b7a9a 100644
--- a/tests/test_jobs.py
+++ b/tests/test_jobs.py
@@ -201,6 +201,19 @@ def test_runs_start_reraises_relogin_exception(requests_mock, dummy_hostname):
         d.runs_start(["main.py"])
 
 
+@pytest.mark.usefixtures("clear_token_file_from_env", "base_mocks")
+def test_run_stop_raises_when_run_id_missing(requests_mock, dummy_hostname):
+    """
+    Confirm that run_stop() without a run_id raises ValueError instead of
+    silently passing None to job_stop. Restores the pre-rename safety:
+    the original signature was `run_stop(self, runId, ...)` (required
+    positional), so calling without args used to raise TypeError.
+    """
+    d = Domino(host=dummy_hostname, project="anyuser/anyproject", api_key="whatever")
+    with pytest.raises(ValueError, match="run_id is required"):
+        d.run_stop()
+
+
 @pytest.mark.usefixtures("clear_token_file_from_env", "base_mocks")
 def test_runs_status_returns_dict(requests_mock, dummy_hostname):
     requests_mock.get(