From fa937bf3a79c8bf4f9f3fba45c86eebc0cf98330 Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 10:22:54 -0300 Subject: [PATCH 1/7] Add Python 3.10 support to CLI --- src/crewai/cli/deploy/utils.py | 55 ++++++++++++++++++++---- tests/cli/deploy/test_deploy_main.py | 62 +++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index 8fe1851df..5a701e675 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -1,11 +1,40 @@ +import sys import re import subprocess -import tomllib - from ..authentication.utils import TokenManager +if sys.version_info >= (3, 11): + import tomllib + + +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: """Get the Git repository's remote URL.""" try: @@ -37,7 +66,7 @@ def get_project_name(pyproject_path: str = "pyproject.toml"): try: # Read the pyproject.toml file with open(pyproject_path, "rb") as f: - pyproject_content = tomllib.load(f) + pyproject_content = parse_toml(f.read()) # Extract the project name project_name = pyproject_content["tool"]["poetry"]["name"] @@ -51,8 +80,12 @@ 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: + 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}") @@ -63,8 +96,8 @@ def get_crewai_version(pyproject_path: str = "pyproject.toml") -> str: """Get the version number of crewai from the pyproject.toml file.""" try: # Read the pyproject.toml file - with open("pyproject.toml", "rb") as f: - pyproject_content = tomllib.load(f) + with open(pyproject_path, "rb") as f: + pyproject_content = parse_toml(f.read()) # Extract the version number of crewai crewai_version = pyproject_content["tool"]["poetry"]["dependencies"]["crewai"][ @@ -77,8 +110,12 @@ def get_crewai_version(pyproject_path: str = "pyproject.toml") -> str: 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: + 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}") diff --git a/tests/cli/deploy/test_deploy_main.py b/tests/cli/deploy/test_deploy_main.py index f4b08d877..52171459a 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,62 @@ class TestDeployCommand(unittest.TestCase): self.assertIn( "Crew 'test_project' removed successfully", fake_out.getvalue() ) + + @patch('crewai.cli.deploy.utils.sys.version_info', (3, 10)) + def test_parse_toml_python_310(self): + toml_content = """ + [tool.poetry] + name = "test_project" + version = "0.1.0" + + [tool.poetry.dependencies] + python = "^3.10" + crewai = "^0.1.0" + """ + parsed = parse_toml(toml_content) + self.assertEqual(parsed['tool']['poetry']['name'], 'test_project') + self.assertEqual(parsed['tool']['poetry']['dependencies']['crewai'], '^0.1.0') + + @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 = "^0.1.0" + """ + parsed = parse_toml(toml_content) + self.assertEqual(parsed['tool']['poetry']['name'], 'test_project') + self.assertEqual(parsed['tool']['poetry']['dependencies']['crewai'], '^0.1.0') + + @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 = "^0.1.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 = "^0.1.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') From 345f1eacde618bd7f78701051e2fc5f5c499ea90 Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 11:14:04 -0300 Subject: [PATCH 2/7] Get current crewai version from poetry.lock --- src/crewai/cli/deploy/utils.py | 38 +++++++++++------------- tests/cli/deploy/test_deploy_main.py | 44 ++++++++++++++++------------ 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index 5a701e675..a7534fc48 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -1,6 +1,7 @@ import sys import re import subprocess +import ast from ..authentication.utils import TokenManager @@ -92,32 +93,27 @@ def get_project_name(pyproject_path: str = "pyproject.toml"): 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_path, "rb") as f: - pyproject_content = parse_toml(f.read()) + 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 if sys.version_info >= (3, 11) else Exception as e: - 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}" - ) + 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_deploy_main.py b/tests/cli/deploy/test_deploy_main.py index 52171459a..a63ea7110 100644 --- a/tests/cli/deploy/test_deploy_main.py +++ b/tests/cli/deploy/test_deploy_main.py @@ -153,21 +153,6 @@ class TestDeployCommand(unittest.TestCase): "Crew 'test_project' removed successfully", fake_out.getvalue() ) - @patch('crewai.cli.deploy.utils.sys.version_info', (3, 10)) - def test_parse_toml_python_310(self): - toml_content = """ - [tool.poetry] - name = "test_project" - version = "0.1.0" - - [tool.poetry.dependencies] - python = "^3.10" - crewai = "^0.1.0" - """ - parsed = parse_toml(toml_content) - self.assertEqual(parsed['tool']['poetry']['name'], 'test_project') - self.assertEqual(parsed['tool']['poetry']['dependencies']['crewai'], '^0.1.0') - @unittest.skipIf(sys.version_info < (3, 11), "Requires Python 3.11+") def test_parse_toml_python_311_plus(self): toml_content = """ @@ -177,11 +162,10 @@ class TestDeployCommand(unittest.TestCase): [tool.poetry.dependencies] python = "^3.11" - crewai = "^0.1.0" + crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" } """ parsed = parse_toml(toml_content) self.assertEqual(parsed['tool']['poetry']['name'], 'test_project') - self.assertEqual(parsed['tool']['poetry']['dependencies']['crewai'], '^0.1.0') @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data=""" [tool.poetry] @@ -190,7 +174,7 @@ class TestDeployCommand(unittest.TestCase): [tool.poetry.dependencies] python = "^3.10" - crewai = "^0.1.0" + 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 @@ -205,9 +189,31 @@ class TestDeployCommand(unittest.TestCase): [tool.poetry.dependencies] python = "^3.11" - crewai = "^0.1.0" + 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()) From 9a10cc15f4bd0838f4d8d6eb22699bc52317c35c Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 14:37:34 -0300 Subject: [PATCH 3/7] Add python 3.10 support back to CLI +fixes --- src/crewai/cli/deploy/api.py | 3 +++ src/crewai/cli/deploy/main.py | 5 +++++ src/crewai/cli/deploy/utils.py | 12 +++++++----- 3 files changed, 15 insertions(+), 5 deletions(-) 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 d67e1cdc8..9cb6f6c8c 100644 --- a/src/crewai/cli/deploy/main.py +++ b/src/crewai/cli/deploy/main.py @@ -113,6 +113,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 a7534fc48..e55309cfa 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -1,10 +1,13 @@ import sys import re import subprocess -import ast + +from rich.console import Console from ..authentication.utils import TokenManager +console = Console() + if sys.version_info >= (3, 11): import tomllib @@ -53,13 +56,12 @@ 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") def get_project_name(pyproject_path: str = "pyproject.toml"): From c8c0a89dc64447fb8b630edee72d0eee63ea4ecb Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 15:02:19 -0300 Subject: [PATCH 4/7] Fix type checking + lint --- src/crewai/cli/deploy/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index e55309cfa..89bb89536 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -39,7 +39,7 @@ def parse_toml(content): return simple_toml_parser(content) -def get_git_remote_url() -> str: +def get_git_remote_url() -> str | None: """Get the Git repository's remote URL.""" try: # Run the git remote -v command @@ -63,6 +63,8 @@ def get_git_remote_url() -> str: except FileNotFoundError: 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"): """Get the project name from the pyproject.toml file.""" @@ -83,7 +85,7 @@ 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 if sys.version_info >= (3, 11) else Exception as e: + 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) From cda1900b14d19e1e101d4b50d2f04b7d50e0e335 Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 15:17:51 -0300 Subject: [PATCH 5/7] Read as `str` no `bytes` +handle when project_name is None (fails, basically) --- src/crewai/cli/deploy/main.py | 4 ++++ src/crewai/cli/deploy/utils.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/crewai/cli/deploy/main.py b/src/crewai/cli/deploy/main.py index 9cb6f6c8c..51afdb5da 100644 --- a/src/crewai/cli/deploy/main.py +++ b/src/crewai/cli/deploy/main.py @@ -33,6 +33,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: diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index 89bb89536..140e486a8 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -66,11 +66,11 @@ def get_git_remote_url() -> str | None: 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: + with open(pyproject_path, "r") as f: pyproject_content = parse_toml(f.read()) # Extract the project name From 5f359b14f74560aecf27930e5042adc04ba6399f Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Thu, 29 Aug 2024 15:58:47 -0300 Subject: [PATCH 6/7] Fix test --- tests/cli/deploy/test_api.py | 1 + 1 file changed, 1 insertion(+) 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" }, ) From 0b9e753c2f3b90e5a44e202a4b8604e67d8c4879 Mon Sep 17 00:00:00 2001 From: Thiago Moretto Date: Fri, 30 Aug 2024 11:52:53 -0300 Subject: [PATCH 7/7] Add comment to warn about dro simple_toml_parser --- src/crewai/cli/deploy/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py index 140e486a8..7579785df 100644 --- a/src/crewai/cli/deploy/utils.py +++ b/src/crewai/cli/deploy/utils.py @@ -13,6 +13,7 @@ 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