From f311a465be4c71897107a29044980ee8f1f4309f Mon Sep 17 00:00:00 2001 From: Vini Brasil Date: Wed, 25 Sep 2024 14:06:20 -0300 Subject: [PATCH] Move crewai.cli.deploy.utils to crewai.cli.utils (#1350) * Prevent double slashes when joining URLs * Move crewai.cli.deploy.utils to crewai.cli.utils This commit moves this package so it's reusable across commands. --- src/crewai/cli/deploy/main.py | 4 +- src/crewai/cli/deploy/utils.py | 155 -------------------------- src/crewai/cli/plus_api.py | 5 +- src/crewai/cli/utils.py | 158 +++++++++++++++++++++++++++ tests/cli/deploy/test_deploy_main.py | 57 ++++++---- 5 files changed, 200 insertions(+), 179 deletions(-) delete mode 100644 src/crewai/cli/deploy/utils.py diff --git a/src/crewai/cli/deploy/main.py b/src/crewai/cli/deploy/main.py index 0772ee088..18ed26c04 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 7579785df..000000000 --- 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 22ccc8352..e829f1199 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 2cb181fc4..363cdd0cf 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 diff --git a/tests/cli/deploy/test_deploy_main.py b/tests/cli/deploy/test_deploy_main.py index a63ea7110..2e8b62a18 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 - project_name = get_project_name() - self.assertEqual(project_name, 'test_project') + from crewai.cli.utils import get_project_name - @patch('builtins.open', new_callable=unittest.mock.mock_open, read_data=""" + 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" @@ -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 - version = get_crewai_version() - self.assertEqual(version, '0.51.1') + from crewai.cli.utils import get_crewai_version - @patch('builtins.open', side_effect=FileNotFoundError) + 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: + 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())