mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-10 00:28:31 +00:00
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.
This commit is contained in:
@@ -3,13 +3,13 @@ from typing import Any, Dict, List, Optional
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from crewai.telemetry import Telemetry
|
from crewai.telemetry import Telemetry
|
||||||
from .api import CrewAPI
|
from crewai.cli.utils import (
|
||||||
from .utils import (
|
|
||||||
fetch_and_json_env_file,
|
fetch_and_json_env_file,
|
||||||
get_auth_token,
|
get_auth_token,
|
||||||
get_git_remote_url,
|
get_git_remote_url,
|
||||||
get_project_name,
|
get_project_name,
|
||||||
)
|
)
|
||||||
|
from .api import CrewAPI
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import requests
|
import requests
|
||||||
from os import getenv
|
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:
|
class PlusAPI:
|
||||||
@@ -19,5 +20,5 @@ class PlusAPI:
|
|||||||
self.base_url = getenv("CREWAI_BASE_URL", "https://app.crewai.com")
|
self.base_url = getenv("CREWAI_BASE_URL", "https://app.crewai.com")
|
||||||
|
|
||||||
def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
|
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)
|
return requests.request(method, url, headers=self.headers, **kwargs)
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
import click
|
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):
|
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)
|
file.write(content)
|
||||||
|
|
||||||
click.secho(f" - Created {dst}", fg="green")
|
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
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from unittest.mock import MagicMock, patch
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from crewai.cli.deploy.main import DeployCommand
|
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):
|
class TestDeployCommand(unittest.TestCase):
|
||||||
@patch("crewai.cli.deploy.main.get_auth_token")
|
@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" }
|
crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" }
|
||||||
"""
|
"""
|
||||||
parsed = parse_toml(toml_content)
|
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]
|
[tool.poetry]
|
||||||
name = "test_project"
|
name = "test_project"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -175,14 +179,19 @@ class TestDeployCommand(unittest.TestCase):
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10"
|
python = "^3.10"
|
||||||
crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" }
|
crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" }
|
||||||
""")
|
""",
|
||||||
|
)
|
||||||
def test_get_project_name_python_310(self, mock_open):
|
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()
|
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+")
|
@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]
|
[tool.poetry]
|
||||||
name = "test_project"
|
name = "test_project"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -190,13 +199,18 @@ class TestDeployCommand(unittest.TestCase):
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.11"
|
python = "^3.11"
|
||||||
crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" }
|
crewai = { extras = ["tools"], version = ">=0.51.0,<1.0.0" }
|
||||||
""")
|
""",
|
||||||
|
)
|
||||||
def test_get_project_name_python_311_plus(self, mock_open):
|
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')
|
|
||||||
|
|
||||||
@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]]
|
[[package]]
|
||||||
name = "crewai"
|
name = "crewai"
|
||||||
version = "0.51.1"
|
version = "0.51.1"
|
||||||
@@ -204,16 +218,19 @@ class TestDeployCommand(unittest.TestCase):
|
|||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10,<4.0"
|
python-versions = ">=3.10,<4.0"
|
||||||
""")
|
""",
|
||||||
|
)
|
||||||
def test_get_crewai_version(self, mock_open):
|
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')
|
|
||||||
|
|
||||||
@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):
|
def test_get_crewai_version_file_not_found(self, mock_open):
|
||||||
from crewai.cli.deploy.utils import get_crewai_version
|
from crewai.cli.utils import get_crewai_version
|
||||||
with patch('sys.stdout', new=StringIO()) as fake_out:
|
|
||||||
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||||
version = get_crewai_version()
|
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())
|
self.assertIn("Error: poetry.lock not found.", fake_out.getvalue())
|
||||||
|
|||||||
Reference in New Issue
Block a user