diff --git a/src/crewai/cli/deploy/api.py b/src/crewai/cli/deploy/api.py index 942fc487e..1037744af 100644 --- a/src/crewai/cli/deploy/api.py +++ b/src/crewai/cli/deploy/api.py @@ -2,6 +2,8 @@ from os import getenv import requests +from crewai.cli.deploy.utils import get_crewai_version + class CrewAPI: """ @@ -13,6 +15,7 @@ class CrewAPI: self.headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", + "User-Agent": f"CrewAI-CLI/{get_crewai_version()}", } self.base_url = getenv( "CREWAI_BASE_URL", "https://dev.crewai.com/crewai_plus/api/v1/crews" diff --git a/src/crewai/cli/deploy/main.py b/src/crewai/cli/deploy/main.py index 67b69a0ee..752100e41 100644 --- a/src/crewai/cli/deploy/main.py +++ b/src/crewai/cli/deploy/main.py @@ -39,6 +39,10 @@ class DeployCommand: raise SystemExit self.project_name = get_project_name() + if self.project_name is None: + console.print("No project name found. Please ensure your project has a valid pyproject.toml file.", style="bold red") + raise SystemExit + self.client = CrewAPI(api_key=access_token) def _handle_error(self, json_response: Dict[str, Any]) -> None: @@ -123,6 +127,11 @@ class DeployCommand: env_vars = fetch_and_json_env_file() remote_repo_url = get_git_remote_url() + if remote_repo_url is None: + console.print("No remote repository URL found.", style="bold red") + console.print("Please ensure your project has a valid remote repository.", style="yellow") + return + self._confirm_input(env_vars, remote_repo_url) payload = self._create_payload(env_vars, remote_repo_url) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index 8fe1851df..7579785df 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -1,12 +1,46 @@ +import sys import re import subprocess -import tomllib +from rich.console import Console from ..authentication.utils import TokenManager +console = Console() -def get_git_remote_url() -> str: + +if sys.version_info >= (3, 11): + import tomllib + + +# Drop the simple_toml_parser when we move to python3.11 +def simple_toml_parser(content): + result = {} + current_section = result + for line in content.split('\n'): + line = line.strip() + if line.startswith('[') and line.endswith(']'): + # New section + section = line[1:-1].split('.') + current_section = result + for key in section: + current_section = current_section.setdefault(key, {}) + elif '=' in line: + key, value = line.split('=', 1) + key = key.strip() + value = value.strip().strip('"') + current_section[key] = value + return result + + +def parse_toml(content): + if sys.version_info >= (3, 11): + return tomllib.loads(content) + else: + return simple_toml_parser(content) + + +def get_git_remote_url() -> str | None: """Get the Git repository's remote URL.""" try: # Run the git remote -v command @@ -23,21 +57,22 @@ def get_git_remote_url() -> str: if matches: return matches[0] # Return the first match (origin URL) else: - print("No origin remote found.") - return "No remote URL found" + console.print("No origin remote found.", style="bold red") except subprocess.CalledProcessError as e: - return f"Error running trying to fetch the Git Repository: {e}" + console.print(f"Error running trying to fetch the Git Repository: {e}", style="bold red") except FileNotFoundError: - return "Git command not found. Make sure Git is installed and in your PATH." + console.print("Git command not found. Make sure Git is installed and in your PATH.", style="bold red") + + return None -def get_project_name(pyproject_path: str = "pyproject.toml"): +def get_project_name(pyproject_path: str = "pyproject.toml") -> str | None: """Get the project name from the pyproject.toml file.""" try: # Read the pyproject.toml file - with open(pyproject_path, "rb") as f: - pyproject_content = tomllib.load(f) + with open(pyproject_path, "r") as f: + pyproject_content = parse_toml(f.read()) # Extract the project name project_name = pyproject_content["tool"]["poetry"]["name"] @@ -51,36 +86,39 @@ def get_project_name(pyproject_path: str = "pyproject.toml"): print(f"Error: {pyproject_path} not found.") except KeyError: print(f"Error: {pyproject_path} is not a valid pyproject.toml file.") - except tomllib.TOMLDecodeError: - print(f"Error: {pyproject_path} is not a valid TOML file.") + except tomllib.TOMLDecodeError if sys.version_info >= (3, 11) else Exception as e: # type: ignore + print( + f"Error: {pyproject_path} is not a valid TOML file." + if sys.version_info >= (3, 11) + else f"Error reading the pyproject.toml file: {e}" + ) except Exception as e: print(f"Error reading the pyproject.toml file: {e}") return None -def get_crewai_version(pyproject_path: str = "pyproject.toml") -> str: - """Get the version number of crewai from the pyproject.toml file.""" +def get_crewai_version(poetry_lock_path: str = "poetry.lock") -> str: + """Get the version number of crewai from the poetry.lock file.""" try: - # Read the pyproject.toml file - with open("pyproject.toml", "rb") as f: - pyproject_content = tomllib.load(f) + with open(poetry_lock_path, "r") as f: + lock_content = f.read() - # Extract the version number of crewai - crewai_version = pyproject_content["tool"]["poetry"]["dependencies"]["crewai"][ - "version" - ] - - return crewai_version + match = re.search( + r'\[\[package\]\]\s*name\s*=\s*"crewai"\s*version\s*=\s*"([^"]+)"', + lock_content, + re.DOTALL, + ) + if match: + return match.group(1) + else: + print("crewai package not found in poetry.lock") + return "no-version-found" except FileNotFoundError: - print(f"Error: {pyproject_path} not found.") - except KeyError: - print(f"Error: {pyproject_path} is not a valid pyproject.toml file.") - except tomllib.TOMLDecodeError: - print(f"Error: {pyproject_path} is not a valid TOML file.") + print(f"Error: {poetry_lock_path} not found.") except Exception as e: - print(f"Error reading the pyproject.toml file: {e}") + print(f"Error reading the poetry.lock file: {e}") return "no-version-found" diff --git a/tests/cli/deploy/test_api.py b/tests/cli/deploy/test_api.py index f1a6c573d..616a9ff3d 100644 --- a/tests/cli/deploy/test_api.py +++ b/tests/cli/deploy/test_api.py @@ -17,6 +17,7 @@ class TestCrewAPI(unittest.TestCase): { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", + "User-Agent": "CrewAI-CLI/no-version-found" }, ) diff --git a/tests/cli/deploy/test_deploy_main.py b/tests/cli/deploy/test_deploy_main.py index f4b08d877..a63ea7110 100644 --- a/tests/cli/deploy/test_deploy_main.py +++ b/tests/cli/deploy/test_deploy_main.py @@ -1,9 +1,10 @@ import unittest from io import StringIO from unittest.mock import MagicMock, patch +import sys from crewai.cli.deploy.main import DeployCommand - +from crewai.cli.deploy.utils import parse_toml class TestDeployCommand(unittest.TestCase): @patch("crewai.cli.deploy.main.get_auth_token") @@ -151,3 +152,68 @@ class TestDeployCommand(unittest.TestCase): self.assertIn( "Crew 'test_project' removed successfully", fake_out.getvalue() ) + + @unittest.skipIf(sys.version_info < (3, 11), "Requires Python 3.11+") + def test_parse_toml_python_311_plus(self): + toml_content = """ + [tool.poetry] + name = "test_project" + version = "0.1.0" + + [tool.poetry.dependencies] + python = "^3.11" + crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" } + """ + parsed = parse_toml(toml_content) + self.assertEqual(parsed['tool']['poetry']['name'], 'test_project') + + @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data=""" + [tool.poetry] + name = "test_project" + version = "0.1.0" + + [tool.poetry.dependencies] + python = "^3.10" + crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" } + """) + def test_get_project_name_python_310(self, mock_open): + from crewai.cli.deploy.utils import get_project_name + project_name = get_project_name() + self.assertEqual(project_name, 'test_project') + + @unittest.skipIf(sys.version_info < (3, 11), "Requires Python 3.11+") + @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data=""" + [tool.poetry] + name = "test_project" + version = "0.1.0" + + [tool.poetry.dependencies] + python = "^3.11" + crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" } + """) + def test_get_project_name_python_311_plus(self, mock_open): + from crewai.cli.deploy.utils import get_project_name + project_name = get_project_name() + self.assertEqual(project_name, 'test_project') + + @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data=""" + [[package]] + name = "crewai" + version = "0.51.1" + description = "Some description" + category = "main" + optional = false + python-versions = ">=3.10,<4.0" + """) + def test_get_crewai_version(self, mock_open): + from crewai.cli.deploy.utils import get_crewai_version + version = get_crewai_version() + self.assertEqual(version, '0.51.1') + + @patch('builtins.open', side_effect=FileNotFoundError) + def test_get_crewai_version_file_not_found(self, mock_open): + from crewai.cli.deploy.utils import get_crewai_version + with patch('sys.stdout', new=StringIO()) as fake_out: + version = get_crewai_version() + self.assertEqual(version, 'no-version-found') + self.assertIn("Error: poetry.lock not found.", fake_out.getvalue())