From ecdf70a2abdc692606aef1756a56736f364f6bc1 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Fri, 7 Mar 2025 10:21:20 -0800 Subject: [PATCH] langgraph-cli-install --- libs/cli-install/.gitignore | 41 +++++++ libs/cli-install/LICENSE | 21 ++++ libs/cli-install/Makefile | 24 ++++ libs/cli-install/README.md | 50 ++++++++ .../langgraph_cli_install/__init__.py | 7 ++ .../cli-install/langgraph_cli_install/main.py | 109 ++++++++++++++++++ libs/cli-install/poetry.lock | 44 +++++++ libs/cli-install/pyproject.toml | 36 ++++++ libs/cli-install/setup.py | 36 ++++++ libs/cli-install/tests/__init__.py | 1 + libs/cli-install/tests/test_main.py | 50 ++++++++ libs/cli-install/tests/test_version.py | 8 ++ 12 files changed, 427 insertions(+) create mode 100644 libs/cli-install/.gitignore create mode 100644 libs/cli-install/LICENSE create mode 100644 libs/cli-install/Makefile create mode 100644 libs/cli-install/README.md create mode 100644 libs/cli-install/langgraph_cli_install/__init__.py create mode 100644 libs/cli-install/langgraph_cli_install/main.py create mode 100644 libs/cli-install/poetry.lock create mode 100644 libs/cli-install/pyproject.toml create mode 100644 libs/cli-install/setup.py create mode 100644 libs/cli-install/tests/__init__.py create mode 100644 libs/cli-install/tests/test_main.py create mode 100644 libs/cli-install/tests/test_version.py diff --git a/libs/cli-install/.gitignore b/libs/cli-install/.gitignore new file mode 100644 index 000000000..7b21a3a37 --- /dev/null +++ b/libs/cli-install/.gitignore @@ -0,0 +1,41 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +env/ +ENV/ + +# Editor +.idea/ +.vscode/ +*.swp +*.swo + +# OS specific +.DS_Store + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ \ No newline at end of file diff --git a/libs/cli-install/LICENSE b/libs/cli-install/LICENSE new file mode 100644 index 000000000..8006a8b87 --- /dev/null +++ b/libs/cli-install/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 LangChain AI, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/libs/cli-install/Makefile b/libs/cli-install/Makefile new file mode 100644 index 000000000..5f074e889 --- /dev/null +++ b/libs/cli-install/Makefile @@ -0,0 +1,24 @@ +.PHONY: install format lint clean build publish + +install: + poetry install + +format: + poetry run ruff format langgraph_cli_install tests + +lint: + poetry run ruff check langgraph_cli_install tests + +test: + poetry run pytest + +clean: + rm -rf dist/ + rm -rf build/ + rm -rf *.egg-info/ + +build: clean + poetry build + +publish: build + poetry publish \ No newline at end of file diff --git a/libs/cli-install/README.md b/libs/cli-install/README.md new file mode 100644 index 000000000..2139e231b --- /dev/null +++ b/libs/cli-install/README.md @@ -0,0 +1,50 @@ +# LangGraph CLI Installer + +A simple installer for the LangGraph CLI that uses `uv` to create an isolated environment. + +## Why? + +This lightweight installer creates an isolated installation of LangGraph CLI without worrying about Python environment conflicts or dependencies. It uses [uv](https://github.com/astral-sh/uv) to create a standalone environment with LangGraph CLI. + +Key benefits: +- Prevents conflicts with other Python packages +- No knowledge of virtual environments needed +- Adds to your PATH automatically +- Installs the latest version of LangGraph CLI + +## Quick Install + +Simply run: + +```bash +pip install langgraph-cli-install && langgraph-cli-install +``` + +This will: +1. Install the uv package if not already installed +2. Create an isolated environment with the latest LangGraph CLI +3. Add the CLI to your PATH automatically + +After installation, you can run `langgraph --help` to get started. + +## How It Works + +This installer is similar to [aider-install](https://github.com/paul-gauthier/aider/blob/main/aider_install/main.py). It: + +1. Uses the `uv` Python installer to create an isolated environment +2. Installs the latest `langgraph-cli` in that environment +3. Adds the installed binary to your PATH + +This approach dramatically reduces installation issues caused by Python environment conflicts. + +## Manual Installation + +If you prefer not to use this installer, you can install LangGraph CLI directly: + +```bash +pip install langgraph-cli +``` + +## License + +MIT \ No newline at end of file diff --git a/libs/cli-install/langgraph_cli_install/__init__.py b/libs/cli-install/langgraph_cli_install/__init__.py new file mode 100644 index 000000000..2b9d73091 --- /dev/null +++ b/libs/cli-install/langgraph_cli_install/__init__.py @@ -0,0 +1,7 @@ +"""LangGraph CLI Installer package.""" + +__version__ = "0.1.0" + +from .main import main + +__all__ = ["main"] diff --git a/libs/cli-install/langgraph_cli_install/main.py b/libs/cli-install/langgraph_cli_install/main.py new file mode 100644 index 000000000..d4795c926 --- /dev/null +++ b/libs/cli-install/langgraph_cli_install/main.py @@ -0,0 +1,109 @@ +"""LangGraph CLI Installation Script. + +Main entry point for installing langgraph-cli in an isolated environment. +This script uses uv to create an isolated installation of langgraph-cli. +""" + +import platform +import subprocess +import sys + +import uv + + +def main(): + """Install langgraph-cli using uv in an isolated environment.""" + print("Installing LangGraph CLI in an isolated environment...") + + try: + uv_bin = uv.find_uv_bin() + + # Get best Python version for installation (prefer 3.12 if available) + python_version = get_latest_python_version() + + # Create an isolated environment with langgraph-cli + print(f"Creating isolated environment using {python_version}...") + subprocess.check_call( + [ + uv_bin, + "tool", + "install", + "--force", + "--python", + python_version, + "langgraph-cli@latest", + ] + ) + + # Update PATH so the tool is available + subprocess.check_call([uv_bin, "tool", "update-shell"]) + + # Show install location and help + show_success_message(uv_bin) + + except subprocess.CalledProcessError as e: + print(f"\nFailed to install langgraph-cli: {e}") + sys.exit(1) + except Exception as e: + print(f"\nAn error occurred: {e}") + sys.exit(1) + + +def get_latest_python_version() -> str: + """Get the latest compatible Python version for installation.""" + # Try to use Python 3.12 if possible, otherwise fall back to the current version + target_version = "3.12" + try: + # Check if this version is available through uv + uv_bin = uv.find_uv_bin() + result = subprocess.run( + [uv_bin, "python", "list"], + capture_output=True, + text=True, + check=False, + ) + if "3.12" in result.stdout: + return f"python{target_version}" + except Exception: + pass + + # Fall back to current version + major, minor = sys.version_info.major, sys.version_info.minor + return f"python{major}.{minor}" + + +def show_success_message(uv_bin): + """Show success message and installation details.""" + # Get installation path + result = subprocess.run( + [uv_bin, "tool", "list"], + capture_output=True, + text=True, + check=True, + ) + + install_path = None + for line in result.stdout.splitlines(): + if "langgraph-cli" in line: + parts = line.strip().split() + if len(parts) >= 2: + install_path = parts[1] + break + + # Success message + print("\nšŸŽ‰ LangGraph CLI has been successfully installed!\n") + print("You can now use it by running:") + print(" langgraph --help") + + if install_path: + print(f"\nInstalled at: {install_path}") + + # Provide hint about shell restart if needed + if platform.system() != "Windows": + print("\nNote: You may need to restart your terminal or run:") + print(" source ~/.bashrc # or ~/.zshrc depending on your shell") + print("to ensure the langgraph command is available in your PATH.") + + +if __name__ == "__main__": + main() diff --git a/libs/cli-install/poetry.lock b/libs/cli-install/poetry.lock new file mode 100644 index 000000000..cfb9b6d9e --- /dev/null +++ b/libs/cli-install/poetry.lock @@ -0,0 +1,44 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "uv" +version = "0.1.45" +description = "An extremely fast Python package installer and resolver, written in Rust." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uv-0.1.45-py3-none-linux_armv6l.whl", hash = "sha256:088af576fb0e0462cd5f718d03fb1a9f16ce5ae61fdb2a9d3ea938fc826cecc1"}, + {file = "uv-0.1.45-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b94180009264f3f7ee74250f8e4f99c8cb0cb3633e3a9c9c66cdef3eb69be575"}, + {file = "uv-0.1.45-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e5d55f0f8b6ae416c72d78106e224c8e8338356da21ddebecc7b1723de80924"}, + {file = "uv-0.1.45-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:7fdb235aaf420fa8ac9009999b1654a23540f03e25c35094543c2f48d7c41aef"}, + {file = "uv-0.1.45-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de81501c0b03160d0944906d1a713f108258360e20c58385974acb7253b56166"}, + {file = "uv-0.1.45-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346aa2d0a4ad3c0c3f7852c1edf5e5a8e5d2ef34c7474e9089877291c2da979c"}, + {file = "uv-0.1.45-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a601eed14d484d36d421e4208911a56aaf758ea6c385ef8edf8ad9f8ead57ce1"}, + {file = "uv-0.1.45-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ca2d5a5e06c5f71c7b213e14fa59129e63b77de3ffbcf84ecc98d647d73a821"}, + {file = "uv-0.1.45-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90b68c80dddebeca69b26a2af1e2e683804bcf2b5f22d107af03d9156d6218c6"}, + {file = "uv-0.1.45-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd7f2f64fdded940342dc37234c11ae3508222c3c9b6b0eac5879dcd586010fa"}, + {file = "uv-0.1.45-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a39141e179fea043151a165c9155031e7976b0e4b076c0c33a45b58a420134e0"}, + {file = "uv-0.1.45-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:68718add6ee2cef2816f9bf8a1dbf2d8cf63d98ddf45840f340029f65a49fd89"}, + {file = "uv-0.1.45-py3-none-musllinux_1_1_i686.whl", hash = "sha256:110e0f45ddb2fe832ce50b0308be90e5439e0c02d3ffe042feeb3f759811f31f"}, + {file = "uv-0.1.45-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:0f6cfe885f109bacc055edd5df2c837616ae2238b9324a9d37835a96b204ab2f"}, + {file = "uv-0.1.45-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:87e77d25e8f358c0d5de1983497ee4cf4cea8fc73373d1ef1063533352db2f89"}, + {file = "uv-0.1.45-py3-none-win32.whl", hash = "sha256:ddb93620c9e01fa83573c2648df4bee3fa548ca940de51c8a2c3566a23a0c776"}, + {file = "uv-0.1.45-py3-none-win_amd64.whl", hash = "sha256:8e2eeea4eec0e09f7d67378152428b5308dba8b33990d045d7a31d19bf18ca1f"}, + {file = "uv-0.1.45.tar.gz", hash = "sha256:40fab956bc7af50dfa4bda14e5871528f57603eb9bf8595eb3144aace0ed8c47"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8.0,<4.0" +content-hash = "ac29a6587488fe83583561554cb37b0812f4609cf34fedc4afae8df804db0d73" diff --git a/libs/cli-install/pyproject.toml b/libs/cli-install/pyproject.toml new file mode 100644 index 000000000..14af11b08 --- /dev/null +++ b/libs/cli-install/pyproject.toml @@ -0,0 +1,36 @@ +[tool.poetry] +name = "langgraph-cli-install" +version = "0.0.1-rc1" +description = "Simple installer for langgraph-cli" +authors = [] +license = "MIT" +readme = "README.md" +repository = "https://www.github.com/langchain-ai/langgraph" +packages = [{ include = "langgraph_cli_install" }] + +[tool.poetry.scripts] +langgraph-cli-install = "langgraph_cli_install.main:main" + +[tool.poetry.dependencies] +python = "^3.8.0,<4.0" +packaging = "^23.0" +uv = "^0.1.24" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +lint.select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # isort + "I", +] +lint.ignore = ["E501", "B008"] diff --git a/libs/cli-install/setup.py b/libs/cli-install/setup.py new file mode 100644 index 000000000..47e2248e6 --- /dev/null +++ b/libs/cli-install/setup.py @@ -0,0 +1,36 @@ +"""Setup script for the langgraph-cli-install package.""" + +from setuptools import find_packages, setup + +if __name__ == "__main__": + setup( + name="langgraph-cli-install", + version="0.1.0", + description="Simple installer for langgraph-cli", + author="", + author_email="", + license="MIT", + packages=find_packages(), + include_package_data=True, + entry_points={ + "console_scripts": [ + "langgraph-cli-install=langgraph_cli_install.main:main", + ], + }, + python_requires=">=3.8", + install_requires=[ + "uv>=0.1.24", + "packaging>=23.0", + ], + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + ) \ No newline at end of file diff --git a/libs/cli-install/tests/__init__.py b/libs/cli-install/tests/__init__.py new file mode 100644 index 000000000..05f02cbc5 --- /dev/null +++ b/libs/cli-install/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for langgraph-cli-install.""" diff --git a/libs/cli-install/tests/test_main.py b/libs/cli-install/tests/test_main.py new file mode 100644 index 000000000..7a3ef140b --- /dev/null +++ b/libs/cli-install/tests/test_main.py @@ -0,0 +1,50 @@ +"""Tests for the main module.""" + +import sys +from unittest.mock import MagicMock, patch + +from langgraph_cli_install.main import get_latest_python_version, main + + +def test_get_latest_python_version(): + """Test that the get_latest_python_version function returns a string.""" + with patch("subprocess.run") as mock_run: + mock_result = MagicMock() + mock_result.stdout = "python3.12" + mock_run.return_value = mock_result + + with patch("uv.find_uv_bin", return_value="/path/to/uv"): + version = get_latest_python_version() + assert isinstance(version, str) + assert "python" in version + + +def test_get_latest_python_version_fallback(): + """Test fallback to current version when 3.12 is not available.""" + with patch("subprocess.run") as mock_run: + mock_result = MagicMock() + mock_result.stdout = "python3.8" # No 3.12 here + mock_run.return_value = mock_result + + with patch("uv.find_uv_bin", return_value="/path/to/uv"): + # Mock sys.version_info + old_version_info = sys.version_info + sys.version_info = MagicMock() + sys.version_info.major = 3 + sys.version_info.minor = 9 + + try: + version = get_latest_python_version() + assert isinstance(version, str) + assert "python3.9" in version + finally: + # Restore original version_info + sys.version_info = old_version_info + + +def test_main_exception(): + """Test main function handles exceptions.""" + with patch("uv.find_uv_bin", side_effect=Exception("Test error")): + with patch("sys.exit") as mock_exit: + main() + mock_exit.assert_called_once_with(1) diff --git a/libs/cli-install/tests/test_version.py b/libs/cli-install/tests/test_version.py new file mode 100644 index 000000000..fe165350b --- /dev/null +++ b/libs/cli-install/tests/test_version.py @@ -0,0 +1,8 @@ +"""Test that the version is defined.""" + +import langgraph_cli_install + + +def test_version(): + """Test that the version is defined.""" + assert langgraph_cli_install.__version__ is not None