From 3ac0067cc3faafb76e155df167d27844c9cce243 Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Thu, 1 Oct 2026 13:25:28 -0300 Subject: [PATCH 1/3] Honor CLI project creation credentials --- roboflow/cli/handlers/project.py | 4 +- tests/cli/test_project_handler.py | 110 ++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/roboflow/cli/handlers/project.py b/roboflow/cli/handlers/project.py index 05e3070d..eb12fc28 100644 --- a/roboflow/cli/handlers/project.py +++ b/roboflow/cli/handlers/project.py @@ -215,12 +215,14 @@ def _get_project(args): # noqa: ANN001 def _create_project(args): # noqa: ANN001 import roboflow from roboflow.cli._output import output, output_error, suppress_sdk_output + from roboflow.config import load_roboflow_api_key annotation = args.annotation if args.annotation else args.name with suppress_sdk_output(args): try: - rf = roboflow.Roboflow() + api_key = args.api_key or load_roboflow_api_key(args.workspace) + rf = roboflow.Roboflow(api_key=api_key) workspace = rf.workspace(args.workspace) except Exception as exc: output_error(args, str(exc)) diff --git a/tests/cli/test_project_handler.py b/tests/cli/test_project_handler.py index 1950e0f5..8e7916cc 100644 --- a/tests/cli/test_project_handler.py +++ b/tests/cli/test_project_handler.py @@ -1,7 +1,11 @@ """Tests for the project CLI handler.""" import json +import os +import tempfile import unittest +from pathlib import Path +from unittest.mock import patch import responses from typer.testing import CliRunner @@ -55,6 +59,112 @@ def test_subcommands_visible(self) -> None: class TestProjectCreateHandler(unittest.TestCase): """project create sends the chosen type and reports server errors.""" + def _mock_create_flow(self, api_key: str, project_type: str) -> None: + from roboflow.config import API_URL + + responses.add(responses.POST, f"{API_URL}/?api_key={api_key}", json={"workspace": "target-ws"}) + responses.add( + responses.GET, + f"{API_URL}/target-ws?api_key={api_key}", + json={"workspace": {"name": "Target", "url": "target-ws", "projects": []}}, + ) + responses.add( + responses.POST, + f"{API_URL}/target-ws/projects?api_key={api_key}", + json={ + "id": "target-ws/clips", + "name": "Clips", + "type": project_type, + "annotation": "Clips", + "classes": {}, + "colors": {}, + "created": 0, + "updated": 0, + "images": 0, + "public": False, + "splits": {}, + "unannotated": 0, + }, + ) + + def _assert_create_flow(self, result, api_key: str, project_type: str) -> None: + from roboflow.config import API_URL + + self.assertEqual(result.exit_code, 0, result.output) + self.assertEqual( + json.loads(result.stdout), + {"id": "target-ws/clips", "name": "Clips", "type": project_type}, + ) + self.assertEqual( + [(call.request.method, call.request.url) for call in responses.calls], + [ + ("POST", f"{API_URL}/?api_key={api_key}"), + ("GET", f"{API_URL}/target-ws?api_key={api_key}"), + ("POST", f"{API_URL}/target-ws/projects?api_key={api_key}"), + ], + ) + self.assertEqual( + json.loads(responses.calls[2].request.body), + {"name": "Clips", "type": project_type, "license": "Private", "annotation": "Clips"}, + ) + + @responses.activate + def test_create_explicit_api_key_overrides_environment_key(self) -> None: + self._mock_create_flow("override-key", "object-detection") + + with patch.dict(os.environ, {"ROBOFLOW_API_KEY": "environment-key"}): + result = runner.invoke( + app, + [ + "--json", + "--api-key", + "override-key", + "--workspace", + "target-ws", + "project", + "create", + "Clips", + "--type", + "object-detection", + ], + ) + + self._assert_create_flow(result, "override-key", "object-detection") + + @responses.activate + def test_create_uses_selected_workspaces_stored_key(self) -> None: + self._mock_create_flow("target-key", "action-recognition") + + with tempfile.TemporaryDirectory() as config_dir: + config_path = Path(config_dir) / "config.json" + config_path.write_text( + json.dumps( + { + "RF_WORKSPACE": "default-ws", + "workspaces": { + "default": {"url": "default-ws", "apiKey": "default-key"}, + "target": {"url": "target-ws", "apiKey": "target-key"}, + }, + } + ) + ) + with patch.dict(os.environ, {"HOME": config_dir, "ROBOFLOW_CONFIG_DIR": str(config_path)}, clear=True): + result = runner.invoke( + app, + [ + "--json", + "--workspace", + "target-ws", + "project", + "create", + "Clips", + "--type", + "action-recognition", + ], + ) + + self._assert_create_flow(result, "target-key", "action-recognition") + @responses.activate def test_create_sends_action_recognition_type(self) -> None: from unittest.mock import patch From 81a2eaf5ac7450f31f0692d5bb78b40f8b479d45 Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Thu, 1 Oct 2026 13:28:51 -0300 Subject: [PATCH 2/3] Keep CLI credential fixture portable on Windows --- tests/cli/test_project_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/cli/test_project_handler.py b/tests/cli/test_project_handler.py index 8e7916cc..9d437d31 100644 --- a/tests/cli/test_project_handler.py +++ b/tests/cli/test_project_handler.py @@ -148,7 +148,11 @@ def test_create_uses_selected_workspaces_stored_key(self) -> None: } ) ) - with patch.dict(os.environ, {"HOME": config_dir, "ROBOFLOW_CONFIG_DIR": str(config_path)}, clear=True): + with patch.dict( + os.environ, + {"HOME": config_dir, "USERPROFILE": config_dir, "ROBOFLOW_CONFIG_DIR": str(config_path)}, + clear=True, + ): result = runner.invoke( app, [ From dd32ce87e22222eec319ababbc7e7f6f75c4220e Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Thu, 1 Oct 2026 14:33:27 -0300 Subject: [PATCH 3/3] Remove duplicate mocked project creation test --- tests/cli/test_project_handler.py | 45 ------------------------------- 1 file changed, 45 deletions(-) diff --git a/tests/cli/test_project_handler.py b/tests/cli/test_project_handler.py index 9d437d31..7eb692ff 100644 --- a/tests/cli/test_project_handler.py +++ b/tests/cli/test_project_handler.py @@ -169,53 +169,8 @@ def test_create_uses_selected_workspaces_stored_key(self) -> None: self._assert_create_flow(result, "target-key", "action-recognition") - @responses.activate - def test_create_sends_action_recognition_type(self) -> None: - from unittest.mock import patch - - from roboflow.config import API_URL - from roboflow.core.workspace import Workspace - - workspace = Workspace( - {"workspace": {"name": "My WS", "url": "my-ws", "projects": []}}, - api_key="fake-key", - default_workspace="my-ws", - model_format="yolov8", - ) - responses.add( - responses.POST, - f"{API_URL}/my-ws/projects", - json={ - "id": "my-ws/clips", - "name": "Clips", - "type": "action-recognition", - "annotation": "Clips", - "classes": {}, - "colors": {}, - "created": 0, - "updated": 0, - "images": 0, - "public": False, - "splits": {}, - "unannotated": 0, - }, - ) - - with patch("roboflow.Roboflow") as mock_rf: - mock_rf.return_value.workspace.return_value = workspace - result = runner.invoke(app, ["--json", "project", "create", "Clips", "--type", "action-recognition"]) - - self.assertEqual(result.exit_code, 0, result.output) - self.assertEqual(len(responses.calls), 1) - self.assertEqual( - json.loads(responses.calls[0].request.body), - {"name": "Clips", "type": "action-recognition", "license": "Private", "annotation": "Clips"}, - ) - @responses.activate def test_create_preserves_server_error_message(self) -> None: - from unittest.mock import patch - from roboflow.config import API_URL from roboflow.core.workspace import Workspace