diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d923003..9cd1b26d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,11 @@ jobs: - name: flake8 run: flake8 . + - name: snake_case + # check_snake_case.py owns the include/exclude rules; pass it everything + # under domino/ and it will skip paths that don't apply. Keeps CI in + # lockstep with the pre-commit hook (both invoke the same script). + run: find domino -name "*.py" | xargs python scripts/check_snake_case.py typecheck: name: Type check diff --git a/CHANGELOG.md b/CHANGELOG.md index 998849e6..72c25d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,36 @@ 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. + +> **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` | +| `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. 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) diff --git a/domino/domino.py b/domino/domino.py index ff918be6..3ff8dce5 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: def __init__( @@ -129,38 +158,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() @@ -184,11 +205,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, @@ -239,27 +260,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 @@ -317,32 +330,22 @@ 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 + ) + 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) - 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). @@ -353,18 +356,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) @@ -382,7 +381,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. @@ -391,11 +390,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
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
diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py
new file mode 100644
index 00000000..37341f05
--- /dev/null
+++ b/tests/test_deprecations.py
@@ -0,0 +1,288 @@
+"""
+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)
+
+
+# ---------------------------------------------------------------------------
+# Both-passed guard: providing both the new and deprecated names should raise
+# ValueError instead of silently letting one win.
+# ---------------------------------------------------------------------------
+
+
+class TestRejectsBothNamesPassed:
+    @pytest.fixture(autouse=True)
+    def _mock_routes(self, requests_mock, dummy_hostname):
+        requests_mock.get(
+            f"{dummy_hostname}/v4/gateway/projects/findProjectByOwnerAndName"
+            "?ownerName=anyuser&projectName=anyproject",
+            json={"id": MOCK_PROJECT_ID},
+        )
+
+    @pytest.mark.usefixtures("clear_token_file_from_env")
+    def test_runs_start_rejects_both_is_direct_and_isDirect(self, client):
+        with pytest.raises(ValueError, match="not both"):
+            client.runs_start("main.py", is_direct=True, isDirect=False)
+
+    @pytest.mark.usefixtures("clear_token_file_from_env")
+    def test_runs_start_rejects_both_commit_id_and_commitId(self, client):
+        with pytest.raises(ValueError, match="not both"):
+            client.runs_start("main.py", commit_id="new", commitId="old")
+
+    @pytest.mark.usefixtures("clear_token_file_from_env")
+    def test_run_stop_rejects_both_run_id_and_runId(self, client):
+        with pytest.raises(ValueError, match="not both"):
+            client.run_stop(run_id=MOCK_RUN_ID, runId=MOCK_RUN_ID)
+
+    @pytest.mark.usefixtures("clear_token_file_from_env")
+    def test_runs_status_rejects_both_run_id_and_runId(self, client):
+        with pytest.raises(ValueError, match="not both"):
+            client.runs_status(run_id=MOCK_RUN_ID, runId=MOCK_RUN_ID)
+
+    @pytest.mark.usefixtures("clear_token_file_from_env")
+    def test_app_publish_rejects_both_app_id_and_appId(self, client):
+        with pytest.raises(ValueError, match="not both"):
+            client.app_publish(app_id=MOCK_APP_ID, appId=MOCK_APP_ID)
+
+    @pytest.mark.usefixtures("clear_token_file_from_env")
+    def test_app_unpublish_rejects_both_app_id_and_appId(self, client):
+        with pytest.raises(ValueError, match="not both"):
+            client.app_unpublish(app_id=MOCK_APP_ID, appId=MOCK_APP_ID)
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(