Repository: crewAIInc/crewAI
Pull request #1350 (2024-09-25 17:02:13)

Move crewai.cli.deploy.utils to crewai.cli.utils
This commit moves this package so it's reusable across commands.

bhancockio: Looks great @vinibrsl !

Patch:
diff --git a/src/crewai/cli/deploy/main.py b/src/crewai/cli/deploy/main.py
index 0772ee08..18ed26c0 100644
--- a/src/crewai/cli/deploy/main.py
+++ b/src/crewai/cli/deploy/main.py
@@ -3,13 +3,13 @@ from typing import Any, Dict, List, Optional
 from rich.console import Console
 
 from crewai.telemetry import Telemetry
-from .api import CrewAPI
-from .utils import (
+from crewai.cli.utils import (
     fetch_and_json_env_file,
     get_auth_token,
     get_git_remote_url,
     get_project_name,
 )
+from .api import CrewAPI
 
 console = Console()
 
diff --git a/src/crewai/cli/deploy/utils.py b/src/crewai/cli/deploy/utils.py
deleted file mode 100644
index 7579785d..00000000
--- a/src/crewai/cli/deploy/utils.py
+++ /dev/null
@@ -1,155 +0,0 @@
-import sys
-import re
-import subprocess
-
-from rich.console import Console
-
-from ..authentication.utils import TokenManager
-
-console = Console()
-
-
-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
-        result = subprocess.run(
-            ["git", "remote", "-v"], capture_output=True, text=True, check=True
-        )
-
-        # Get the output
-        output = result.stdout
-
-        # Parse the output to find the origin URL
-        matches = re.findall(r"origin\s+(.*?)\s+\(fetch\)", output)
-
-        if matches:
-            return matches[0]  # Return the first match (origin URL)
-        else:
-            console.print("No origin remote found.", style="bold red")
-
-    except subprocess.CalledProcessError as e:
-        console.print(f"Error running trying to fetch the Git Repository: {e}", style="bold red")
-    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") -> str | None:
-    """Get the project name from the pyproject.toml file."""
-    try:
-        # Read the pyproject.toml file
-        with open(pyproject_path, "r") as f:
-            pyproject_content = parse_toml(f.read())
-
-        # Extract the project name
-        project_name = pyproject_content["tool"]["poetry"]["name"]
-
-        if "crewai" not in pyproject_content["tool"]["poetry"]["dependencies"]:
-            raise Exception("crewai is not in the dependencies.")
-
-        return project_name
-
-    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:  # 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(poetry_lock_path: str = "poetry.lock") -> str:
-    """Get the version number of crewai from the poetry.lock file."""
-    try:
-        with open(poetry_lock_path, "r") as f:
-            lock_content = f.read()
-
-        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: {poetry_lock_path} not found.")
-    except Exception as e:
-        print(f"Error reading the poetry.lock file: {e}")
-
-    return "no-version-found"
-
-
-def fetch_and_json_env_file(env_file_path: str = ".env") -> dict:
-    """Fetch the environment variables from a .env file and return them as a dictionary."""
-    try:
-        # Read the .env file
-        with open(env_file_path, "r") as f:
-            env_content = f.read()
-
-        # Parse the .env file content to a dictionary
-        env_dict = {}
-        for line in env_content.splitlines():
-            if line.strip() and not line.strip().startswith("#"):
-                key, value = line.split("=", 1)
-                env_dict[key.strip()] = value.strip()
-
-        return env_dict
-
-    except FileNotFoundError:
-        print(f"Error: {env_file_path} not found.")
-    except Exception as e:
-        print(f"Error reading the .env file: {e}")
-
-    return {}
-
-
-def get_auth_token() -> str:
-    """Get the authentication token."""
-    access_token = TokenManager().get_token()
-    if not access_token:
-        raise Exception()
-    return access_token
diff --git a/src/crewai/cli/plus_api.py b/src/crewai/cli/plus_api.py
index 22ccc835..e829f119 100644
--- a/src/crewai/cli/plus_api.py
+++ b/src/crewai/cli/plus_api.py
@@ -1,6 +1,7 @@
 import requests
 from os import getenv
-from crewai.cli.deploy.utils import get_crewai_version
+from crewai.cli.utils import get_crewai_version
+from urllib.parse import urljoin
 
 
 class PlusAPI:
@@ -19,5 +20,5 @@ class PlusAPI:
         self.base_url = getenv("CREWAI_BASE_URL", "https://app.crewai.com")
 
     def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
-        url = f"{self.base_url}/{endpoint}"
+        url = urljoin(self.base_url, endpoint)
         return requests.request(method, url, headers=self.headers, **kwargs)
diff --git a/src/crewai/cli/utils.py b/src/crewai/cli/utils.py
index 2cb181fc..363cdd0c 100644
--- a/src/crewai/cli/utils.py
+++ b/src/crewai/cli/utils.py
@@ -1,4 +1,15 @@
 import click
+import re
+import subprocess
+import sys
+
+from rich.console import Console
+from crewai.cli.authentication.utils import TokenManager
+
+if sys.version_info >= (3, 11):
+    import tomllib
+
+console = Console()
 
 
 def copy_template(src, dst, name, class_name, folder_name):
@@ -16,3 +27,150 @@ def copy_template(src, dst, name, class_name, folder_name):
         file.write(content)
 
     click.secho(f"  - Created {dst}", fg="green")
+
+
+# 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
+        result = subprocess.run(
+            ["git", "remote", "-v"], capture_output=True, text=True, check=True
+        )
+
+        # Get the output
+        output = result.stdout
+
+        # Parse the output to find the origin URL
+        matches = re.findall(r"origin\s+(.*?)\s+\(fetch\)", output)
+
+        if matches:
+            return matches[0]  # Return the first match (origin URL)
+        else:
+            console.print("No origin remote found.", style="bold red")
+
+    except subprocess.CalledProcessError as e:
+        console.print(
+            f"Error running trying to fetch the Git Repository: {e}", style="bold red"
+        )
+    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") -> str | None:
+    """Get the project name from the pyproject.toml file."""
+    try:
+        # Read the pyproject.toml file
+        with open(pyproject_path, "r") as f:
+            pyproject_content = parse_toml(f.read())
+
+        # Extract the project name
+        project_name = pyproject_content["tool"]["poetry"]["name"]
+
+        if "crewai" not in pyproject_content["tool"]["poetry"]["dependencies"]:
+            raise Exception("crewai is not in the dependencies.")
+
+        return project_name
+
+    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:  # 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(poetry_lock_path: str = "poetry.lock") -> str:
+    """Get the version number of crewai from the poetry.lock file."""
+    try:
+        with open(poetry_lock_path, "r") as f:
+            lock_content = f.read()
+
+        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: {poetry_lock_path} not found.")
+    except Exception as e:
+        print(f"Error reading the poetry.lock file: {e}")
+
+    return "no-version-found"
+
+
+def fetch_and_json_env_file(env_file_path: str = ".env") -> dict:
+    """Fetch the environment variables from a .env file and return them as a dictionary."""
+    try:
+        # Read the .env file
+        with open(env_file_path, "r") as f:
+            env_content = f.read()
+
+        # Parse the .env file content to a dictionary
+        env_dict = {}
+        for line in env_content.splitlines():
+            if line.strip() and not line.strip().startswith("#"):
+                key, value = line.split("=", 1)
+                env_dict[key.strip()] = value.strip()
+
+        return env_dict
+
+    except FileNotFoundError:
+        print(f"Error: {env_file_path} not found.")
+    except Exception as e:
+        print(f"Error reading the .env file: {e}")
+
+    return {}
+
+
+def get_auth_token() -> str:
+    """Get the authentication token."""
+    access_token = TokenManager().get_token()
+    if not access_token:
+        raise Exception()
+    return access_token


Test patch:
diff --git a/tests/cli/deploy/test_deploy_main.py b/tests/cli/deploy/test_deploy_main.py
index a63ea711..2e8b62a1 100644
--- a/tests/cli/deploy/test_deploy_main.py
+++ b/tests/cli/deploy/test_deploy_main.py
@@ -4,7 +4,8 @@ from unittest.mock import MagicMock, patch
 import sys
 
 from crewai.cli.deploy.main import DeployCommand
-from crewai.cli.deploy.utils import parse_toml
+from crewai.cli.utils import parse_toml
+
 
 class TestDeployCommand(unittest.TestCase):
     @patch("crewai.cli.deploy.main.get_auth_token")
@@ -165,9 +166,12 @@ class TestDeployCommand(unittest.TestCase):
         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"]["name"], "test_project")
 
-    @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data="""
+    @patch(
+        "builtins.open",
+        new_callable=unittest.mock.mock_open,
+        read_data="""
     [tool.poetry]
     name = "test_project"
     version = "0.1.0"
@@ -175,14 +179,19 @@ class TestDeployCommand(unittest.TestCase):
     [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
+        from crewai.cli.utils import get_project_name
+
         project_name = get_project_name()
-        self.assertEqual(project_name, 'test_project')
+        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="""
+    @patch(
+        "builtins.open",
+        new_callable=unittest.mock.mock_open,
+        read_data="""
     [tool.poetry]
     name = "test_project"
     version = "0.1.0"
@@ -190,13 +199,18 @@ class TestDeployCommand(unittest.TestCase):
     [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
+        from crewai.cli.utils import get_project_name
+
         project_name = get_project_name()
-        self.assertEqual(project_name, 'test_project')
+        self.assertEqual(project_name, "test_project")
 
-    @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data="""
+    @patch(
+        "builtins.open",
+        new_callable=unittest.mock.mock_open,
+        read_data="""
     [[package]]
     name = "crewai"
     version = "0.51.1"
@@ -204,16 +218,19 @@ class TestDeployCommand(unittest.TestCase):
     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
+        from crewai.cli.utils import get_crewai_version
+
         version = get_crewai_version()
-        self.assertEqual(version, '0.51.1')
+        self.assertEqual(version, "0.51.1")
 
-    @patch('builtins.open', side_effect=FileNotFoundError)
+    @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:
+        from crewai.cli.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.assertEqual(version, "no-version-found")
             self.assertIn("Error: poetry.lock not found.", fake_out.getvalue())