Repository: crewAIInc/crewAI
Pull request #1269 (2024-08-29 17:57:59)

Add py 3.10 support back to CLI + fixes
* Fallback to simple parser to get project name when running Python 3.10.
* Get crewai version from poetry.lock.
* Send locked crewai version as User-Agent header.


Patch:
diff --git a/src/crewai/cli/deploy/api.py b/src/crewai/cli/deploy/api.py
index 942fc487..1037744a 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 d67e1cdc..51afdb5d 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:
@@ -113,6 +117,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 8fe1851d..7579785d 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)
-
-        # Extract the version number of crewai
-        crewai_version = pyproject_content["tool"]["poetry"]["dependencies"]["crewai"][
-            "version"
-        ]
+        with open(poetry_lock_path, "r") as f:
+            lock_content = f.read()
 
-        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"
 


Test patch:
diff --git a/tests/cli/deploy/test_api.py b/tests/cli/deploy/test_api.py
index f1a6c573..616a9ff3 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 f4b08d87..a63ea711 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())