diff --git a/libertai_client/commands/agent.py b/libertai_client/commands/agent.py index 40571e4..a8f7df2 100644 --- a/libertai_client/commands/agent.py +++ b/libertai_client/commands/agent.py @@ -3,6 +3,7 @@ from typing import Annotated import aiohttp +import questionary import rich import typer from dotenv import dotenv_values @@ -12,7 +13,11 @@ from libertai_client.config import config from libertai_client.interfaces.agent import AgentPythonPackageManager, AgentUsageType from libertai_client.utils.agent import parse_agent_config_env, create_agent_zip -from libertai_client.utils.python import detect_python_project_version +from libertai_client.utils.python import ( + detect_python_project_version, + detect_python_dependencies_management, + validate_python_version, +) from libertai_client.utils.system import get_full_path from libertai_client.utils.typer import AsyncTyper @@ -20,6 +25,25 @@ err_console = Console(stderr=True) +dependencies_management_choices: list[questionary.Choice] = [ + questionary.Choice( + title="poetry", + value=AgentPythonPackageManager.poetry, + description="poetry-style pyproject.toml and poetry.lock", + ), + questionary.Choice( + title="requirements.txt", + value=AgentPythonPackageManager.pip, + description="Any management tool that outputs a requirements.txt file (pip, pip-tools...)", + ), + questionary.Choice( + title="pyproject.toml", + value="TODO", + description="Any tool respecting the standard PEP 621 pyproject.toml (hatch, modern usage of setuptools...)", + disabled="Coming soon", + ), +] + @app.command() async def deploy( @@ -27,7 +51,7 @@ async def deploy( python_version: Annotated[ str | None, typer.Option(help="Version to deploy with", prompt=False) ] = None, - package_manager: Annotated[ + dependencies_management: Annotated[ AgentPythonPackageManager | None, typer.Option( help="Package manager used to handle dependencies", @@ -55,15 +79,42 @@ async def deploy( err_console.print(f"[red]{error}") raise typer.Exit(1) - # TODO: try to detect package manager, show detected value and ask user for the confirmation or change - if package_manager is None: - package_manager = AgentPythonPackageManager.poetry + if dependencies_management is None: + # Trying to find the way dependencies are managed + detected_dependencies_management = detect_python_dependencies_management(path) + # Confirming with the user (or asking if none found) + dependencies_management = await questionary.select( + "Dependencies management", + choices=dependencies_management_choices, + default=next( + ( + choice + for choice in dependencies_management_choices + if choice.value == detected_dependencies_management.value + ), + None, + ), + show_description=True, + ).ask_async() + if dependencies_management is None: + err_console.print( + "[red]You must select the way Python dependencies are managed." + ) + raise typer.Exit(1) if python_version is None: # Trying to find the python version - detected_python_version = detect_python_project_version(path, package_manager) + detected_python_version = detect_python_project_version( + path, dependencies_management + ) # Confirming the version with the user (or asking if none found) - python_version = typer.prompt("Python version", default=detected_python_version) + python_version = await questionary.text( + "Python version", + default=detected_python_version + if detected_python_version is not None + else "", + validate=validate_python_version, + ).ask_async() agent_zip_path = "/tmp/libertai-agent.zip" create_agent_zip(path, agent_zip_path) @@ -71,7 +122,7 @@ async def deploy( data = aiohttp.FormData() data.add_field("secret", libertai_config.secret) data.add_field("python_version", python_version) - data.add_field("package_manager", package_manager.value) + data.add_field("package_manager", dependencies_management.value) data.add_field("usage_type", usage_type.value) data.add_field("code", open(agent_zip_path, "rb"), filename="libertai-agent.zip") @@ -83,10 +134,12 @@ async def deploy( ) as response: if response.status == 200: response_data = UpdateAgentResponse(**json.loads(await response.text())) # noqa: F821 - # TODO: don't show /docs if deployed in python mode - rich.print( - f"[green]Agent successfully deployed on http://[{response_data.instance_ip}]:8000/docs" + success_text = ( + f"Agent successfully deployed on http://[{response_data.instance_ip}]:8000/docs" + if usage_type == AgentUsageType.fastapi + else f"Agent successfully deployed on instance {response_data.instance_ip}" ) + rich.print(f"[green]{success_text}") else: error_message = await response.text() err_console.print(f"[red]Request failed\n{error_message}") diff --git a/libertai_client/utils/python.py b/libertai_client/utils/python.py index 3e07a5a..88bc37c 100644 --- a/libertai_client/utils/python.py +++ b/libertai_client/utils/python.py @@ -9,6 +9,12 @@ from libertai_client.utils.system import get_full_path +def validate_python_version(version: str) -> bool: + if re.match(r"^3(?:\.\d+){0,2}$", version): + return True + return False + + def __fetch_real_python_versions() -> list[str]: response = requests.get( "https://api.github.com/repos/python/cpython/tags?per_page=100" @@ -16,7 +22,7 @@ def __fetch_real_python_versions() -> list[str]: if response.status_code == 200: releases = response.json() versions = [str(release["name"]).removeprefix("v") for release in releases] - exact_versions = [v for v in versions if re.match(r"^\d+\.\d+\.\d+$", v)] + exact_versions = [v for v in versions if validate_python_version(v)] return exact_versions else: return [] @@ -62,3 +68,18 @@ def detect_python_project_version( # TODO: if pyproject, look in pyproject.toml # TODO: if pip, look in requirements.txt return None + + +def detect_python_dependencies_management( + project_path: str, +) -> AgentPythonPackageManager: + try: + _poetry_lock_path = get_full_path(project_path, "poetry.lock") + # Path was found without throwing an error, its poetry + return AgentPythonPackageManager.poetry + except FileNotFoundError: + pass + + # TODO: confirm with requirements.txt + # TODO: handle pyproject.toml standard compatible package managers + return AgentPythonPackageManager.pip diff --git a/poetry.lock b/poetry.lock index 70ec628..476a237 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1511,6 +1511,20 @@ files = [ {file = "poetry_core-2.0.0.tar.gz", hash = "sha256:3317a3cc3932011a61114236b2d49883f4fb1403d2f5e97771ac0d077cfa396f"}, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.48" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, +] + +[package.dependencies] +wcwidth = "*" + [[package]] name = "propcache" version = "0.2.1" @@ -1832,6 +1846,20 @@ files = [ {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] +[[package]] +name = "questionary" +version = "2.1.0" +description = "Python library to build pretty command line user prompts ⭐️" +optional = false +python-versions = ">=3.8" +files = [ + {file = "questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec"}, + {file = "questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587"}, +] + +[package.dependencies] +prompt_toolkit = ">=2.0,<4.0" + [[package]] name = "referencing" version = "0.35.1" @@ -2237,6 +2265,17 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [[package]] name = "web3" version = "6.20.3" @@ -2449,4 +2488,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "d87f760dd3ce2a42426af69fd1754ac50edbb6414797d177e6aca871aecb0107" +content-hash = "ba994a8e743f3ab9fe2be33403ba1333fb5023c7c6ba9bd769618aa837dc31fb" diff --git a/pyproject.toml b/pyproject.toml index 0e6da15..5510534 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ python-dotenv = "^1.0.1" libertai-utils = "0.0.9" pathspec = "^0.12.1" poetry-core = "^2.0.0" +questionary = "^2.1.0" [tool.poetry.group.dev.dependencies] mypy = "^1.11.1"