From 51754899a2b3e35d94f225bef82cb2957cb1d2b3 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Fri, 20 Feb 2026 18:21:05 -0500 Subject: [PATCH] feat: migrate CLI http client from requests to httpx --- lib/crewai/pyproject.toml | 1 + .../src/crewai/cli/authentication/main.py | 8 +-- lib/crewai/src/crewai/cli/command.py | 13 ++-- lib/crewai/src/crewai/cli/enterprise/main.py | 10 ++-- .../src/crewai/cli/organization/main.py | 14 ++--- lib/crewai/src/crewai/cli/plus_api.py | 59 +++++++++---------- lib/crewai/src/crewai/cli/provider.py | 20 +++---- .../cli/authentication/test_auth_main.py | 12 ++-- .../tests/cli/deploy/test_deploy_main.py | 21 +++---- lib/crewai/tests/cli/enterprise/test_main.py | 23 +++++--- .../tests/cli/organization/test_main.py | 16 +++-- lib/crewai/tests/cli/test_plus_api.py | 55 +++++++++-------- lib/crewai/tests/cli/tools/test_main.py | 2 +- lib/crewai/tests/cli/triggers/test_main.py | 12 ++-- uv.lock | 2 + 15 files changed, 138 insertions(+), 130 deletions(-) diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index ff1866696..b4b3e2ecc 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "json5~=0.10.0", "portalocker~=2.7.0", "pydantic-settings~=2.10.1", + "httpx~=0.28.1", "mcp~=1.26.0", "uv~=0.9.13", "aiosqlite~=0.21.0", diff --git a/lib/crewai/src/crewai/cli/authentication/main.py b/lib/crewai/src/crewai/cli/authentication/main.py index 996fd7c63..7bbda61d5 100644 --- a/lib/crewai/src/crewai/cli/authentication/main.py +++ b/lib/crewai/src/crewai/cli/authentication/main.py @@ -2,8 +2,8 @@ import time from typing import TYPE_CHECKING, Any, TypeVar, cast import webbrowser +import httpx from pydantic import BaseModel, Field -import requests from rich.console import Console from crewai.cli.authentication.utils import validate_jwt_token @@ -98,7 +98,7 @@ class AuthenticationCommand: "scope": " ".join(self.oauth2_provider.get_oauth_scopes()), "audience": self.oauth2_provider.get_audience(), } - response = requests.post( + response = httpx.post( url=self.oauth2_provider.get_authorize_url(), data=device_code_payload, timeout=20, @@ -130,7 +130,7 @@ class AuthenticationCommand: attempts = 0 while True and attempts < 10: - response = requests.post( + response = httpx.post( self.oauth2_provider.get_token_url(), data=token_payload, timeout=30 ) token_data = response.json() @@ -149,7 +149,7 @@ class AuthenticationCommand: return if token_data["error"] not in ("authorization_pending", "slow_down"): - raise requests.HTTPError( + raise httpx.HTTPError( token_data.get("error_description") or token_data.get("error") ) diff --git a/lib/crewai/src/crewai/cli/command.py b/lib/crewai/src/crewai/cli/command.py index 3f85318fb..139f69373 100644 --- a/lib/crewai/src/crewai/cli/command.py +++ b/lib/crewai/src/crewai/cli/command.py @@ -1,5 +1,6 @@ -import requests -from requests.exceptions import JSONDecodeError +import json + +import httpx from rich.console import Console from crewai.cli.authentication.token import get_auth_token @@ -30,16 +31,16 @@ class PlusAPIMixin: console.print("Run 'crewai login' to sign up/login.", style="bold green") raise SystemExit from None - def _validate_response(self, response: requests.Response) -> None: + def _validate_response(self, response: httpx.Response) -> None: """ Handle and display error messages from API responses. Args: - response (requests.Response): The response from the Plus API + response (httpx.Response): The response from the Plus API """ try: json_response = response.json() - except (JSONDecodeError, ValueError): + except (json.JSONDecodeError, ValueError): console.print( "Failed to parse response from Enterprise API failed. Details:", style="bold red", @@ -62,7 +63,7 @@ class PlusAPIMixin: ) raise SystemExit - if not response.ok: + if not response.is_success: console.print( "Request to Enterprise API failed. Details:", style="bold red" ) diff --git a/lib/crewai/src/crewai/cli/enterprise/main.py b/lib/crewai/src/crewai/cli/enterprise/main.py index 2a73f1ae0..395de418b 100644 --- a/lib/crewai/src/crewai/cli/enterprise/main.py +++ b/lib/crewai/src/crewai/cli/enterprise/main.py @@ -1,7 +1,7 @@ +import json from typing import Any, cast -import requests -from requests.exceptions import JSONDecodeError, RequestException +import httpx from rich.console import Console from crewai.cli.authentication.main import Oauth2Settings, ProviderFactory @@ -47,12 +47,12 @@ class EnterpriseConfigureCommand(BaseCommand): "User-Agent": f"CrewAI-CLI/{get_crewai_version()}", "X-Crewai-Version": get_crewai_version(), } - response = requests.get(oauth_endpoint, timeout=30, headers=headers) + response = httpx.get(oauth_endpoint, timeout=30, headers=headers) response.raise_for_status() try: oauth_config = response.json() - except JSONDecodeError as e: + except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON response from {oauth_endpoint}") from e self._validate_oauth_config(oauth_config) @@ -62,7 +62,7 @@ class EnterpriseConfigureCommand(BaseCommand): ) return cast(dict[str, Any], oauth_config) - except RequestException as e: + except httpx.HTTPError as e: raise ValueError(f"Failed to connect to enterprise URL: {e!s}") from e except Exception as e: raise ValueError(f"Error fetching OAuth2 configuration: {e!s}") from e diff --git a/lib/crewai/src/crewai/cli/organization/main.py b/lib/crewai/src/crewai/cli/organization/main.py index 4ee954698..fe61ec202 100644 --- a/lib/crewai/src/crewai/cli/organization/main.py +++ b/lib/crewai/src/crewai/cli/organization/main.py @@ -1,4 +1,4 @@ -from requests import HTTPError +from httpx import HTTPStatusError from rich.console import Console from rich.table import Table @@ -10,11 +10,11 @@ console = Console() class OrganizationCommand(BaseCommand, PlusAPIMixin): - def __init__(self): + def __init__(self) -> None: BaseCommand.__init__(self) PlusAPIMixin.__init__(self, telemetry=self._telemetry) - def list(self): + def list(self) -> None: try: response = self.plus_api_client.get_organizations() response.raise_for_status() @@ -33,7 +33,7 @@ class OrganizationCommand(BaseCommand, PlusAPIMixin): table.add_row(org["name"], org["uuid"]) console.print(table) - except HTTPError as e: + except HTTPStatusError as e: if e.response.status_code == 401: console.print( "You are not logged in to any organization. Use 'crewai login' to login.", @@ -50,7 +50,7 @@ class OrganizationCommand(BaseCommand, PlusAPIMixin): ) raise SystemExit(1) from e - def switch(self, org_id): + def switch(self, org_id: str) -> None: try: response = self.plus_api_client.get_organizations() response.raise_for_status() @@ -72,7 +72,7 @@ class OrganizationCommand(BaseCommand, PlusAPIMixin): f"Successfully switched to {org['name']} ({org['uuid']})", style="bold green", ) - except HTTPError as e: + except HTTPStatusError as e: if e.response.status_code == 401: console.print( "You are not logged in to any organization. Use 'crewai login' to login.", @@ -87,7 +87,7 @@ class OrganizationCommand(BaseCommand, PlusAPIMixin): console.print(f"Failed to switch organization: {e!s}", style="bold red") raise SystemExit(1) from e - def current(self): + def current(self) -> None: settings = Settings() if settings.org_uuid: console.print( diff --git a/lib/crewai/src/crewai/cli/plus_api.py b/lib/crewai/src/crewai/cli/plus_api.py index e07d44d10..cbe402eff 100644 --- a/lib/crewai/src/crewai/cli/plus_api.py +++ b/lib/crewai/src/crewai/cli/plus_api.py @@ -3,7 +3,6 @@ from typing import Any from urllib.parse import urljoin import httpx -import requests from crewai.cli.config import Settings from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL @@ -43,16 +42,16 @@ class PlusAPI: def _make_request( self, method: str, endpoint: str, **kwargs: Any - ) -> requests.Response: + ) -> httpx.Response: url = urljoin(self.base_url, endpoint) - session = requests.Session() - session.trust_env = False - return session.request(method, url, headers=self.headers, **kwargs) + verify = kwargs.pop("verify", True) + with httpx.Client(trust_env=False, verify=verify) as client: + return client.request(method, url, headers=self.headers, **kwargs) - def login_to_tool_repository(self) -> requests.Response: + def login_to_tool_repository(self) -> httpx.Response: return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login") - def get_tool(self, handle: str) -> requests.Response: + def get_tool(self, handle: str) -> httpx.Response: return self._make_request("GET", f"{self.TOOLS_RESOURCE}/{handle}") async def get_agent(self, handle: str) -> httpx.Response: @@ -68,7 +67,7 @@ class PlusAPI: description: str | None, encoded_file: str, available_exports: list[dict[str, Any]] | None = None, - ) -> requests.Response: + ) -> httpx.Response: params = { "handle": handle, "public": is_public, @@ -79,54 +78,52 @@ class PlusAPI: } return self._make_request("POST", f"{self.TOOLS_RESOURCE}", json=params) - def deploy_by_name(self, project_name: str) -> requests.Response: + def deploy_by_name(self, project_name: str) -> httpx.Response: return self._make_request( "POST", f"{self.CREWS_RESOURCE}/by-name/{project_name}/deploy" ) - def deploy_by_uuid(self, uuid: str) -> requests.Response: + def deploy_by_uuid(self, uuid: str) -> httpx.Response: return self._make_request("POST", f"{self.CREWS_RESOURCE}/{uuid}/deploy") - def crew_status_by_name(self, project_name: str) -> requests.Response: + def crew_status_by_name(self, project_name: str) -> httpx.Response: return self._make_request( "GET", f"{self.CREWS_RESOURCE}/by-name/{project_name}/status" ) - def crew_status_by_uuid(self, uuid: str) -> requests.Response: + def crew_status_by_uuid(self, uuid: str) -> httpx.Response: return self._make_request("GET", f"{self.CREWS_RESOURCE}/{uuid}/status") def crew_by_name( self, project_name: str, log_type: str = "deployment" - ) -> requests.Response: + ) -> httpx.Response: return self._make_request( "GET", f"{self.CREWS_RESOURCE}/by-name/{project_name}/logs/{log_type}" ) - def crew_by_uuid( - self, uuid: str, log_type: str = "deployment" - ) -> requests.Response: + def crew_by_uuid(self, uuid: str, log_type: str = "deployment") -> httpx.Response: return self._make_request( "GET", f"{self.CREWS_RESOURCE}/{uuid}/logs/{log_type}" ) - def delete_crew_by_name(self, project_name: str) -> requests.Response: + def delete_crew_by_name(self, project_name: str) -> httpx.Response: return self._make_request( "DELETE", f"{self.CREWS_RESOURCE}/by-name/{project_name}" ) - def delete_crew_by_uuid(self, uuid: str) -> requests.Response: + def delete_crew_by_uuid(self, uuid: str) -> httpx.Response: return self._make_request("DELETE", f"{self.CREWS_RESOURCE}/{uuid}") - def list_crews(self) -> requests.Response: + def list_crews(self) -> httpx.Response: return self._make_request("GET", self.CREWS_RESOURCE) - def create_crew(self, payload: dict[str, Any]) -> requests.Response: + def create_crew(self, payload: dict[str, Any]) -> httpx.Response: return self._make_request("POST", self.CREWS_RESOURCE, json=payload) - def get_organizations(self) -> requests.Response: + def get_organizations(self) -> httpx.Response: return self._make_request("GET", self.ORGANIZATIONS_RESOURCE) - def initialize_trace_batch(self, payload: dict[str, Any]) -> requests.Response: + def initialize_trace_batch(self, payload: dict[str, Any]) -> httpx.Response: return self._make_request( "POST", f"{self.TRACING_RESOURCE}/batches", @@ -136,7 +133,7 @@ class PlusAPI: def initialize_ephemeral_trace_batch( self, payload: dict[str, Any] - ) -> requests.Response: + ) -> httpx.Response: return self._make_request( "POST", f"{self.EPHEMERAL_TRACING_RESOURCE}/batches", @@ -145,7 +142,7 @@ class PlusAPI: def send_trace_events( self, trace_batch_id: str, payload: dict[str, Any] - ) -> requests.Response: + ) -> httpx.Response: return self._make_request( "POST", f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}/events", @@ -155,7 +152,7 @@ class PlusAPI: def send_ephemeral_trace_events( self, trace_batch_id: str, payload: dict[str, Any] - ) -> requests.Response: + ) -> httpx.Response: return self._make_request( "POST", f"{self.EPHEMERAL_TRACING_RESOURCE}/batches/{trace_batch_id}/events", @@ -165,7 +162,7 @@ class PlusAPI: def finalize_trace_batch( self, trace_batch_id: str, payload: dict[str, Any] - ) -> requests.Response: + ) -> httpx.Response: return self._make_request( "PATCH", f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}/finalize", @@ -175,7 +172,7 @@ class PlusAPI: def finalize_ephemeral_trace_batch( self, trace_batch_id: str, payload: dict[str, Any] - ) -> requests.Response: + ) -> httpx.Response: return self._make_request( "PATCH", f"{self.EPHEMERAL_TRACING_RESOURCE}/batches/{trace_batch_id}/finalize", @@ -185,7 +182,7 @@ class PlusAPI: def mark_trace_batch_as_failed( self, trace_batch_id: str, error_message: str - ) -> requests.Response: + ) -> httpx.Response: return self._make_request( "PATCH", f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}", @@ -193,13 +190,11 @@ class PlusAPI: timeout=30, ) - def get_triggers(self) -> requests.Response: + def get_triggers(self) -> httpx.Response: """Get all available triggers from integrations.""" return self._make_request("GET", f"{self.INTEGRATIONS_RESOURCE}/apps") - def get_trigger_payload( - self, app_slug: str, trigger_slug: str - ) -> requests.Response: + def get_trigger_payload(self, app_slug: str, trigger_slug: str) -> httpx.Response: """Get sample payload for a specific trigger.""" return self._make_request( "GET", f"{self.INTEGRATIONS_RESOURCE}/{app_slug}/{trigger_slug}/payload" diff --git a/lib/crewai/src/crewai/cli/provider.py b/lib/crewai/src/crewai/cli/provider.py index 6de337b85..1f1e4ec40 100644 --- a/lib/crewai/src/crewai/cli/provider.py +++ b/lib/crewai/src/crewai/cli/provider.py @@ -8,7 +8,7 @@ from typing import Any import certifi import click -import requests +import httpx from crewai.cli.constants import JSON_URL, MODELS, PROVIDERS @@ -165,20 +165,20 @@ def fetch_provider_data(cache_file: Path) -> dict[str, Any] | None: ssl_config = os.environ["SSL_CERT_FILE"] = certifi.where() try: - response = requests.get(JSON_URL, stream=True, timeout=60, verify=ssl_config) - response.raise_for_status() - data = download_data(response) - with open(cache_file, "w") as f: - json.dump(data, f) - return data - except requests.RequestException as e: + with httpx.stream("GET", JSON_URL, timeout=60, verify=ssl_config) as response: + response.raise_for_status() + data = download_data(response) + with open(cache_file, "w") as f: + json.dump(data, f) + return data + except httpx.HTTPError as e: click.secho(f"Error fetching provider data: {e}", fg="red") except json.JSONDecodeError: click.secho("Error parsing provider data. Invalid JSON format.", fg="red") return None -def download_data(response: requests.Response) -> dict[str, Any]: +def download_data(response: httpx.Response) -> dict[str, Any]: """Downloads data from a given HTTP response and returns the JSON content. Args: @@ -194,7 +194,7 @@ def download_data(response: requests.Response) -> dict[str, Any]: with click.progressbar( length=total_size, label="Downloading", show_pos=True ) as bar: - for chunk in response.iter_content(block_size): + for chunk in response.iter_bytes(block_size): if chunk: data_chunks.append(chunk) bar.update(len(chunk)) diff --git a/lib/crewai/tests/cli/authentication/test_auth_main.py b/lib/crewai/tests/cli/authentication/test_auth_main.py index 5f7308e20..095fea3c4 100644 --- a/lib/crewai/tests/cli/authentication/test_auth_main.py +++ b/lib/crewai/tests/cli/authentication/test_auth_main.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock, call, patch import pytest -import requests +import httpx from crewai.cli.authentication.main import AuthenticationCommand from crewai.cli.constants import ( CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE, @@ -220,7 +220,7 @@ class TestAuthenticationCommand: ] mock_console_print.assert_has_calls(expected_calls) - @patch("requests.post") + @patch("crewai.cli.authentication.main.httpx.post") def test_get_device_code(self, mock_post): mock_response = MagicMock() mock_response.json.return_value = { @@ -256,7 +256,7 @@ class TestAuthenticationCommand: "verification_uri_complete": "https://example.com/auth", } - @patch("requests.post") + @patch("crewai.cli.authentication.main.httpx.post") @patch("crewai.cli.authentication.main.console.print") def test_poll_for_token_success(self, mock_console_print, mock_post): mock_response_success = MagicMock() @@ -305,7 +305,7 @@ class TestAuthenticationCommand: ] mock_console_print.assert_has_calls(expected_calls) - @patch("requests.post") + @patch("crewai.cli.authentication.main.httpx.post") @patch("crewai.cli.authentication.main.console.print") def test_poll_for_token_timeout(self, mock_console_print, mock_post): mock_response_pending = MagicMock() @@ -324,7 +324,7 @@ class TestAuthenticationCommand: "Timeout: Failed to get the token. Please try again.", style="bold red" ) - @patch("requests.post") + @patch("crewai.cli.authentication.main.httpx.post") def test_poll_for_token_error(self, mock_post): """Test the method to poll for token (error path).""" # Setup mock to return error @@ -338,5 +338,5 @@ class TestAuthenticationCommand: device_code_data = {"device_code": "test_device_code", "interval": 1} - with pytest.raises(requests.HTTPError): + with pytest.raises(httpx.HTTPError): self.auth_command._poll_for_token(device_code_data) diff --git a/lib/crewai/tests/cli/deploy/test_deploy_main.py b/lib/crewai/tests/cli/deploy/test_deploy_main.py index f33dfbbd5..4b818cc58 100644 --- a/lib/crewai/tests/cli/deploy/test_deploy_main.py +++ b/lib/crewai/tests/cli/deploy/test_deploy_main.py @@ -4,10 +4,11 @@ from io import StringIO from unittest.mock import MagicMock, Mock, patch import pytest -import requests +import json + +import httpx from crewai.cli.deploy.main import DeployCommand from crewai.cli.utils import parse_toml -from requests.exceptions import JSONDecodeError class TestDeployCommand(unittest.TestCase): @@ -37,18 +38,18 @@ class TestDeployCommand(unittest.TestCase): DeployCommand() def test_validate_response_successful_response(self): - mock_response = Mock(spec=requests.Response) + mock_response = Mock(spec=httpx.Response) mock_response.json.return_value = {"message": "Success"} mock_response.status_code = 200 - mock_response.ok = True + mock_response.is_success = True with patch("sys.stdout", new=StringIO()) as fake_out: self.deploy_command._validate_response(mock_response) assert fake_out.getvalue() == "" def test_validate_response_json_decode_error(self): - mock_response = Mock(spec=requests.Response) - mock_response.json.side_effect = JSONDecodeError("Decode error", "", 0) + mock_response = Mock(spec=httpx.Response) + mock_response.json.side_effect = json.JSONDecodeError("Decode error", "", 0) mock_response.status_code = 500 mock_response.content = b"Invalid JSON" @@ -64,13 +65,13 @@ class TestDeployCommand(unittest.TestCase): assert "Response:\nInvalid JSON" in output def test_validate_response_422_error(self): - mock_response = Mock(spec=requests.Response) + mock_response = Mock(spec=httpx.Response) mock_response.json.return_value = { "field1": ["Error message 1"], "field2": ["Error message 2"], } mock_response.status_code = 422 - mock_response.ok = False + mock_response.is_success = False with patch("sys.stdout", new=StringIO()) as fake_out: with pytest.raises(SystemExit): @@ -84,10 +85,10 @@ class TestDeployCommand(unittest.TestCase): assert "Field2 Error message 2" in output def test_validate_response_other_error(self): - mock_response = Mock(spec=requests.Response) + mock_response = Mock(spec=httpx.Response) mock_response.json.return_value = {"error": "Something went wrong"} mock_response.status_code = 500 - mock_response.ok = False + mock_response.is_success = False with patch("sys.stdout", new=StringIO()) as fake_out: with pytest.raises(SystemExit): diff --git a/lib/crewai/tests/cli/enterprise/test_main.py b/lib/crewai/tests/cli/enterprise/test_main.py index e6be4e006..8a225dc41 100644 --- a/lib/crewai/tests/cli/enterprise/test_main.py +++ b/lib/crewai/tests/cli/enterprise/test_main.py @@ -3,8 +3,9 @@ import unittest from pathlib import Path from unittest.mock import Mock, patch -import requests -from requests.exceptions import JSONDecodeError +import json + +import httpx from crewai.cli.enterprise.main import EnterpriseConfigureCommand from crewai.cli.settings.main import SettingsCommand @@ -25,7 +26,7 @@ class TestEnterpriseConfigureCommand(unittest.TestCase): def tearDown(self): shutil.rmtree(self.test_dir) - @patch('crewai.cli.enterprise.main.requests.get') + @patch('crewai.cli.enterprise.main.httpx.get') @patch('crewai.cli.enterprise.main.get_crewai_version') def test_successful_configuration(self, mock_get_version, mock_requests_get): mock_get_version.return_value = "1.0.0" @@ -73,19 +74,23 @@ class TestEnterpriseConfigureCommand(unittest.TestCase): self.assertEqual(call_args[0], key) self.assertEqual(call_args[1], value) - @patch('crewai.cli.enterprise.main.requests.get') + @patch('crewai.cli.enterprise.main.httpx.get') @patch('crewai.cli.enterprise.main.get_crewai_version') def test_http_error_handling(self, mock_get_version, mock_requests_get): mock_get_version.return_value = "1.0.0" mock_response = Mock() - mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found") + mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( + "404 Not Found", + request=httpx.Request("GET", "http://test"), + response=httpx.Response(404), + ) mock_requests_get.return_value = mock_response with self.assertRaises(SystemExit): self.enterprise_command.configure("https://enterprise.example.com") - @patch('crewai.cli.enterprise.main.requests.get') + @patch('crewai.cli.enterprise.main.httpx.get') @patch('crewai.cli.enterprise.main.get_crewai_version') def test_invalid_json_response(self, mock_get_version, mock_requests_get): mock_get_version.return_value = "1.0.0" @@ -93,13 +98,13 @@ class TestEnterpriseConfigureCommand(unittest.TestCase): mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status.return_value = None - mock_response.json.side_effect = JSONDecodeError("Invalid JSON", "", 0) + mock_response.json.side_effect = json.JSONDecodeError("Invalid JSON", "", 0) mock_requests_get.return_value = mock_response with self.assertRaises(SystemExit): self.enterprise_command.configure("https://enterprise.example.com") - @patch('crewai.cli.enterprise.main.requests.get') + @patch('crewai.cli.enterprise.main.httpx.get') @patch('crewai.cli.enterprise.main.get_crewai_version') def test_missing_required_fields(self, mock_get_version, mock_requests_get): mock_get_version.return_value = "1.0.0" @@ -115,7 +120,7 @@ class TestEnterpriseConfigureCommand(unittest.TestCase): with self.assertRaises(SystemExit): self.enterprise_command.configure("https://enterprise.example.com") - @patch('crewai.cli.enterprise.main.requests.get') + @patch('crewai.cli.enterprise.main.httpx.get') @patch('crewai.cli.enterprise.main.get_crewai_version') def test_settings_update_error(self, mock_get_version, mock_requests_get): mock_get_version.return_value = "1.0.0" diff --git a/lib/crewai/tests/cli/organization/test_main.py b/lib/crewai/tests/cli/organization/test_main.py index c0620fe33..0db790cbb 100644 --- a/lib/crewai/tests/cli/organization/test_main.py +++ b/lib/crewai/tests/cli/organization/test_main.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch, call import pytest from click.testing import CliRunner -import requests +import httpx from crewai.cli.organization.main import OrganizationCommand from crewai.cli.cli import org_list, switch, current @@ -115,7 +115,7 @@ class TestOrganizationCommand(unittest.TestCase): def test_list_organizations_api_error(self, mock_console): self.org_command.plus_api_client = MagicMock() self.org_command.plus_api_client.get_organizations.side_effect = ( - requests.exceptions.RequestException("API Error") + httpx.HTTPError("API Error") ) with pytest.raises(SystemExit): @@ -201,8 +201,10 @@ class TestOrganizationCommand(unittest.TestCase): @patch("crewai.cli.organization.main.console") def test_list_organizations_unauthorized(self, mock_console): mock_response = MagicMock() - mock_http_error = requests.exceptions.HTTPError( - "401 Client Error: Unauthorized", response=MagicMock(status_code=401) + mock_http_error = httpx.HTTPStatusError( + "401 Client Error: Unauthorized", + request=httpx.Request("GET", "http://test"), + response=httpx.Response(401), ) mock_response.raise_for_status.side_effect = mock_http_error @@ -219,8 +221,10 @@ class TestOrganizationCommand(unittest.TestCase): @patch("crewai.cli.organization.main.console") def test_switch_organization_unauthorized(self, mock_console): mock_response = MagicMock() - mock_http_error = requests.exceptions.HTTPError( - "401 Client Error: Unauthorized", response=MagicMock(status_code=401) + mock_http_error = httpx.HTTPStatusError( + "401 Client Error: Unauthorized", + request=httpx.Request("GET", "http://test"), + response=httpx.Response(401), ) mock_response.raise_for_status.side_effect = mock_http_error diff --git a/lib/crewai/tests/cli/test_plus_api.py b/lib/crewai/tests/cli/test_plus_api.py index 70eff917e..94728db20 100644 --- a/lib/crewai/tests/cli/test_plus_api.py +++ b/lib/crewai/tests/cli/test_plus_api.py @@ -33,9 +33,9 @@ class TestPlusAPI(unittest.TestCase): self.assertEqual(response, mock_response) def assert_request_with_org_id( - self, mock_make_request, method: str, endpoint: str, **kwargs + self, mock_client_instance, method: str, endpoint: str, **kwargs ): - mock_make_request.assert_called_once_with( + mock_client_instance.request.assert_called_once_with( method, f"{os.getenv('CREWAI_PLUS_URL')}{endpoint}", headers={ @@ -49,24 +49,25 @@ class TestPlusAPI(unittest.TestCase): ) @patch("crewai.cli.plus_api.Settings") - @patch("requests.Session.request") + @patch("crewai.cli.plus_api.httpx.Client") def test_login_to_tool_repository_with_org_uuid( - self, mock_make_request, mock_settings_class + self, mock_client_class, mock_settings_class ): mock_settings = MagicMock() mock_settings.org_uuid = self.org_uuid mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL') mock_settings_class.return_value = mock_settings - # re-initialize Client self.api = PlusAPI(self.api_key) + mock_client_instance = MagicMock() mock_response = MagicMock() - mock_make_request.return_value = mock_response + mock_client_instance.request.return_value = mock_response + mock_client_class.return_value.__enter__.return_value = mock_client_instance response = self.api.login_to_tool_repository() self.assert_request_with_org_id( - mock_make_request, "POST", "/crewai_plus/api/v1/tools/login" + mock_client_instance, "POST", "/crewai_plus/api/v1/tools/login" ) self.assertEqual(response, mock_response) @@ -82,23 +83,23 @@ class TestPlusAPI(unittest.TestCase): self.assertEqual(response, mock_response) @patch("crewai.cli.plus_api.Settings") - @patch("requests.Session.request") - def test_get_tool_with_org_uuid(self, mock_make_request, mock_settings_class): + @patch("crewai.cli.plus_api.httpx.Client") + def test_get_tool_with_org_uuid(self, mock_client_class, mock_settings_class): mock_settings = MagicMock() mock_settings.org_uuid = self.org_uuid mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL') mock_settings_class.return_value = mock_settings - # re-initialize Client self.api = PlusAPI(self.api_key) - # Set up mock response + mock_client_instance = MagicMock() mock_response = MagicMock() - mock_make_request.return_value = mock_response + mock_client_instance.request.return_value = mock_response + mock_client_class.return_value.__enter__.return_value = mock_client_instance response = self.api.get_tool("test_tool_handle") self.assert_request_with_org_id( - mock_make_request, "GET", "/crewai_plus/api/v1/tools/test_tool_handle" + mock_client_instance, "GET", "/crewai_plus/api/v1/tools/test_tool_handle" ) self.assertEqual(response, mock_response) @@ -130,18 +131,18 @@ class TestPlusAPI(unittest.TestCase): self.assertEqual(response, mock_response) @patch("crewai.cli.plus_api.Settings") - @patch("requests.Session.request") - def test_publish_tool_with_org_uuid(self, mock_make_request, mock_settings_class): + @patch("crewai.cli.plus_api.httpx.Client") + def test_publish_tool_with_org_uuid(self, mock_client_class, mock_settings_class): mock_settings = MagicMock() mock_settings.org_uuid = self.org_uuid mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL') mock_settings_class.return_value = mock_settings - # re-initialize Client self.api = PlusAPI(self.api_key) - # Set up mock response + mock_client_instance = MagicMock() mock_response = MagicMock() - mock_make_request.return_value = mock_response + mock_client_instance.request.return_value = mock_response + mock_client_class.return_value.__enter__.return_value = mock_client_instance handle = "test_tool_handle" public = True @@ -153,7 +154,6 @@ class TestPlusAPI(unittest.TestCase): handle, public, version, description, encoded_file ) - # Expected params including organization_uuid expected_params = { "handle": handle, "public": public, @@ -164,7 +164,7 @@ class TestPlusAPI(unittest.TestCase): } self.assert_request_with_org_id( - mock_make_request, "POST", "/crewai_plus/api/v1/tools", json=expected_params + mock_client_instance, "POST", "/crewai_plus/api/v1/tools", json=expected_params ) self.assertEqual(response, mock_response) @@ -195,20 +195,19 @@ class TestPlusAPI(unittest.TestCase): ) self.assertEqual(response, mock_response) - @patch("crewai.cli.plus_api.requests.Session") - def test_make_request(self, mock_session): + @patch("crewai.cli.plus_api.httpx.Client") + def test_make_request(self, mock_client_class): + mock_client_instance = MagicMock() mock_response = MagicMock() - - mock_session_instance = mock_session.return_value - mock_session_instance.request.return_value = mock_response + mock_client_instance.request.return_value = mock_response + mock_client_class.return_value.__enter__.return_value = mock_client_instance response = self.api._make_request("GET", "test_endpoint") - mock_session.assert_called_once() - mock_session_instance.request.assert_called_once_with( + mock_client_class.assert_called_once_with(trust_env=False, verify=True) + mock_client_instance.request.assert_called_once_with( "GET", f"{self.api.base_url}/test_endpoint", headers=self.api.headers ) - mock_session_instance.trust_env = False self.assertEqual(response, mock_response) @patch("crewai.cli.plus_api.PlusAPI._make_request") diff --git a/lib/crewai/tests/cli/tools/test_main.py b/lib/crewai/tests/cli/tools/test_main.py index 71acea76d..6661011d3 100644 --- a/lib/crewai/tests/cli/tools/test_main.py +++ b/lib/crewai/tests/cli/tools/test_main.py @@ -351,7 +351,7 @@ def test_publish_api_error( mock_response = MagicMock() mock_response.status_code = 500 mock_response.json.return_value = {"error": "Internal Server Error"} - mock_response.ok = False + mock_response.is_success = False mock_publish.return_value = mock_response with raises(SystemExit): diff --git a/lib/crewai/tests/cli/triggers/test_main.py b/lib/crewai/tests/cli/triggers/test_main.py index 93d24568d..641abc7cf 100644 --- a/lib/crewai/tests/cli/triggers/test_main.py +++ b/lib/crewai/tests/cli/triggers/test_main.py @@ -3,7 +3,7 @@ import subprocess import unittest from unittest.mock import Mock, patch -import requests +import httpx from crewai.cli.triggers.main import TriggersCommand @@ -21,7 +21,7 @@ class TestTriggersCommand(unittest.TestCase): @patch("crewai.cli.triggers.main.console.print") def test_list_triggers_success(self, mock_console_print): - mock_response = Mock(spec=requests.Response) + mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 mock_response.ok = True mock_response.json.return_value = { @@ -50,7 +50,7 @@ class TestTriggersCommand(unittest.TestCase): @patch("crewai.cli.triggers.main.console.print") def test_list_triggers_no_apps(self, mock_console_print): - mock_response = Mock(spec=requests.Response) + mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 mock_response.ok = True mock_response.json.return_value = {"apps": []} @@ -81,7 +81,7 @@ class TestTriggersCommand(unittest.TestCase): @patch("crewai.cli.triggers.main.console.print") @patch.object(TriggersCommand, "_run_crew_with_payload") def test_execute_with_trigger_success(self, mock_run_crew, mock_console_print): - mock_response = Mock(spec=requests.Response) + mock_response = Mock(spec=httpx.Response) mock_response.status_code = 200 mock_response.ok = True mock_response.json.return_value = { @@ -99,7 +99,7 @@ class TestTriggersCommand(unittest.TestCase): @patch("crewai.cli.triggers.main.console.print") def test_execute_with_trigger_not_found(self, mock_console_print): - mock_response = Mock(spec=requests.Response) + mock_response = Mock(spec=httpx.Response) mock_response.status_code = 404 mock_response.json.return_value = {"error": "Trigger not found"} self.mock_client.get_trigger_payload.return_value = mock_response @@ -159,7 +159,7 @@ class TestTriggersCommand(unittest.TestCase): @patch("crewai.cli.triggers.main.console.print") def test_execute_with_trigger_with_default_error_message(self, mock_console_print): - mock_response = Mock(spec=requests.Response) + mock_response = Mock(spec=httpx.Response) mock_response.status_code = 404 mock_response.json.return_value = {} self.mock_client.get_trigger_payload.return_value = mock_response diff --git a/uv.lock b/uv.lock index df8cb3430..f49801c32 100644 --- a/uv.lock +++ b/uv.lock @@ -1096,6 +1096,7 @@ dependencies = [ { name = "appdirs" }, { name = "chromadb" }, { name = "click" }, + { name = "httpx" }, { name = "instructor" }, { name = "json-repair" }, { name = "json5" }, @@ -1195,6 +1196,7 @@ requires-dist = [ { name = "crewai-tools", marker = "extra == 'tools'", editable = "lib/crewai-tools" }, { name = "docling", marker = "extra == 'docling'", specifier = "~=2.63.0" }, { name = "google-genai", marker = "extra == 'google-genai'", specifier = "~=1.49.0" }, + { name = "httpx", specifier = "~=0.28.1" }, { name = "httpx-auth", marker = "extra == 'a2a'", specifier = "~=0.23.1" }, { name = "httpx-sse", marker = "extra == 'a2a'", specifier = "~=0.4.0" }, { name = "ibm-watsonx-ai", marker = "extra == 'watson'", specifier = "~=1.3.39" },