diff --git a/.github/tape_collection/cli_config.tape b/.github/tape_collection/cli_config.tape new file mode 100644 index 00000000..869b7262 --- /dev/null +++ b/.github/tape_collection/cli_config.tape @@ -0,0 +1,13 @@ +Output docs/cli/demos/config.gif + +Set FontSize 20 +Set Width 1000 +Set Height 150 +Set Padding 20 + +Type "runpod config" +Enter + +Sleep 4s + +Type "" diff --git a/.github/tape_collection/cli_help.tape b/.github/tape_collection/cli_help.tape new file mode 100644 index 00000000..b1aa08b1 --- /dev/null +++ b/.github/tape_collection/cli_help.tape @@ -0,0 +1,11 @@ +Output docs/cli/demos/help.gif + +Set FontSize 20 +Set Width 1000 +Set Height 500 +Set Padding 20 + +Type "runpod --help" +Enter + +Sleep 6s diff --git a/.github/tape_collection/cli_ssh.tape b/.github/tape_collection/cli_ssh.tape new file mode 100644 index 00000000..2d1716c7 --- /dev/null +++ b/.github/tape_collection/cli_ssh.tape @@ -0,0 +1,16 @@ +Output docs/cli/demos/ssh.gif + +Set FontSize 20 +Set Width 1000 +Set Height 200 +Set Padding 20 + +Type "runpod ssh add-key" +Enter + +Sleep 4s + +Type "y" +Enter + +Sleep 4s diff --git a/.github/workflows/vhs.yml b/.github/workflows/vhs.yml new file mode 100644 index 00000000..253ce34f --- /dev/null +++ b/.github/workflows/vhs.yml @@ -0,0 +1,43 @@ +name: vhs +on: + push: + # paths: + # - vhs.tape + + workflow_dispatch: + +jobs: + vhs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install runpod + run: | + pip install . + + # runpod --help + - uses: charmbracelet/vhs-action@v1 + with: + path: ".github/tape_collection/cli_help.tape" + + # runpod config + - uses: charmbracelet/vhs-action@v1 + with: + path: ".github/tape_collection/cli_config.tape" + + # runpod ssh + - uses: charmbracelet/vhs-action@v1 + with: + path: ".github/tape_collection/cli_ssh.tape" + + # Add gifs to commit + - uses: stefanzweifel/git-auto-commit-action@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + commit_message: Update generated VHS GIF + commit_user_name: vhs-action 📼 + commit_user_email: actions@github.com + commit_author: vhs-action 📼 + file_pattern: "*.gif" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d08acf9..11cb0b80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,9 @@ ### Added -- BETA: CLI DevEx functionality to create development projects. - `test_output` can be passed in as an arg to compare the results of `test_input` - Generator/Streaming handlers supported with local testing +- [BETA] CLI DevEx functionality to create development projects. ## Release 1.3.0 (10/12/23) diff --git a/docs/cli/demos/config.gif b/docs/cli/demos/config.gif new file mode 100644 index 00000000..74a2ef8e Binary files /dev/null and b/docs/cli/demos/config.gif differ diff --git a/docs/cli/demos/help.gif b/docs/cli/demos/help.gif new file mode 100644 index 00000000..8bc28bb8 Binary files /dev/null and b/docs/cli/demos/help.gif differ diff --git a/docs/cli/demos/ssh.gif b/docs/cli/demos/ssh.gif new file mode 100644 index 00000000..39f96e7e Binary files /dev/null and b/docs/cli/demos/ssh.gif differ diff --git a/docs/cli/command_line_interface.md b/docs/cli/references/command_line_interface.md similarity index 100% rename from docs/cli/command_line_interface.md rename to docs/cli/references/command_line_interface.md diff --git a/docs/cli/projects.md b/docs/cli/references/projects.md similarity index 100% rename from docs/cli/projects.md rename to docs/cli/references/projects.md diff --git a/docs/cli/runpod.toml.md b/docs/cli/references/runpod.toml.md similarity index 100% rename from docs/cli/runpod.toml.md rename to docs/cli/references/runpod.toml.md diff --git a/docs/cli/start_here.md b/docs/cli/start_here.md new file mode 100644 index 00000000..11d72d75 --- /dev/null +++ b/docs/cli/start_here.md @@ -0,0 +1,17 @@ +# [BETA] | RunPod Python CLI Reference + +Note: This CLI is not the same as runpodctl and provides a different set of features. + +## Getting Started + +![runpod --help](demos/help.gif) + +### Configure + +![runpod config](demos/config.gif) + +Store your RunPod API key by running `runpod config`. Optionally you can also call the command with your API key `runpod config YOUR_API_KEY` or include the `--profile` to save multiple keys (stored under "default" profile is not specified) Credentials are stored in `~/.runpod/config.toml`. + +![runpod ssh add-key](demos/ssh.gif) + +Add a SSH key to you account by running `runpod ssh add-key`. To specify and existing key pass in `--key` or `--key-file` to use a file. Keys are stored in `~/.runpod/ssh/`. If no key is specified a new one will be generated and stored. diff --git a/runpod/cli/groups/project/functions.py b/runpod/cli/groups/project/functions.py index 446c9a82..0eef6f2a 100644 --- a/runpod/cli/groups/project/functions.py +++ b/runpod/cli/groups/project/functions.py @@ -10,7 +10,7 @@ import tomlkit from tomlkit import document, comment, table, nl -from runpod import get_pod +from runpod import get_pod, __version__ from runpod.cli.utils.ssh_cmd import SSHConnection from .helpers import get_project_pod, copy_template_files, attempt_pod_launch, load_project_config from ...utils.rp_sync import sync_directory @@ -33,17 +33,30 @@ def create_new_project(project_name, runpod_volume_id, python_version, # pylint: copy_template_files(template_dir, project_folder) + # Replace placeholders in requirements.txt + requirements_path = os.path.join(project_folder, "builder/requirements.txt") + with open(requirements_path, 'r', encoding='utf-8') as requirements_file: + requirements_content = requirements_file.read() + + if "dev" in __version__: + requirements_content = requirements_content.replace( + '<>', 'git+https://github.com/runpod/runpod-python.git') + else: + requirements_content = requirements_content.replace( + '<>', f'runpod=={__version__}') + + with open(requirements_path, 'w', encoding='utf-8') as requirements_file: + requirements_file.write(requirements_content) + # If there's a model_name, replace placeholders in handler.py if model_name: - handler_path = os.path.join(project_name, "handler.py") - if os.path.exists(handler_path): - with open(handler_path, 'r', encoding='utf-8') as file: - handler_content = file.read() - + handler_path = os.path.join(project_name, "src/handler.py") + with open(handler_path, 'r', encoding='utf-8') as file: + handler_content = file.read() handler_content = handler_content.replace('<>', model_name) - with open(handler_path, 'w', encoding='utf-8') as file: - file.write(handler_content) + with open(handler_path, 'w', encoding='utf-8') as file: + file.write(handler_content) else: project_folder = os.getcwd() diff --git a/runpod/cli/groups/project/starter_templates/default/builder/requirements.txt b/runpod/cli/groups/project/starter_templates/default/builder/requirements.txt index 755cd042..8fa962d6 100644 --- a/runpod/cli/groups/project/starter_templates/default/builder/requirements.txt +++ b/runpod/cli/groups/project/starter_templates/default/builder/requirements.txt @@ -1,3 +1,8 @@ -# List your python dependencies here. See https://pip.pypa.io/en/stable/user_guide/#requirements-files +# Required Python packages get listed here, one per line. +# Reccomended to lock the version number to avoid unexpected changes. -runpod==1.3.0 +# You can also install packages from a git repository, e.g.: +# git+https://github.com/runpod/runpod-python.git +# To learn more, see https://pip.pypa.io/en/stable/reference/requirements-file-format/ + +<> diff --git a/runpod/cli/groups/project/starter_templates/llama2/requirements.txt b/runpod/cli/groups/project/starter_templates/llama2/builder/requirements.txt similarity index 100% rename from runpod/cli/groups/project/starter_templates/llama2/requirements.txt rename to runpod/cli/groups/project/starter_templates/llama2/builder/requirements.txt diff --git a/runpod/cli/groups/project/starter_templates/llama2/handler.py b/runpod/cli/groups/project/starter_templates/llama2/src/handler.py similarity index 100% rename from runpod/cli/groups/project/starter_templates/llama2/handler.py rename to runpod/cli/groups/project/starter_templates/llama2/src/handler.py diff --git a/tests/test_cli/test_cli_groups/test_project_functions.py b/tests/test_cli/test_cli_groups/test_project_functions.py index ffeb2478..c29a66ce 100644 --- a/tests/test_cli/test_cli_groups/test_project_functions.py +++ b/tests/test_cli/test_cli_groups/test_project_functions.py @@ -75,7 +75,7 @@ def test_create_runpod_toml(self, mock_open_file, mock_exists): with patch("runpod.cli.groups.project.functions.copy_template_files"): create_new_project("test_project", "volume_id", "3.8") toml_file_location = os.path.join(os.getcwd(), "test_project", "runpod.toml") - mock_open_file.assert_called_once_with(toml_file_location, 'w', encoding="UTF-8") # pylint: disable=line-too-long + mock_open_file.assert_called_with(toml_file_location, 'w', encoding="UTF-8") # pylint: disable=line-too-long assert mock_exists.called @patch('runpod.cli.groups.project.functions.get_project_pod') @@ -89,6 +89,27 @@ def test_existing_project_pod(self, mock_get_pod): launch_project() mock_print.assert_called_with('Project pod already launched. Run "runpod project start" to start.') # pylint: disable=line-too-long + @patch("os.path.exists", return_value=True) + @patch("builtins.open", new_callable=mock_open, read_data="<> placeholder") + def test_update_requirements_file(self, mock_open_file, mock_exists): + """ Test that placeholders in requirements.txt are replaced correctly. """ + with patch("runpod.cli.groups.project.functions.__version__", "dev"), \ + patch("runpod.cli.groups.project.functions.copy_template_files"): + create_new_project("test_project", "volume_id", "3.8") + assert mock_open_file.called + assert mock_exists.called + + @patch("os.path.exists", return_value=True) + @patch("builtins.open", new_callable=mock_open, read_data="<> placeholder") + def test_update_requirements_file_non_dev(self, mock_open_file, mock_exists): + """ Test that placeholders in requirements.txt are replaced for non-dev versions. """ + with patch("runpod.cli.groups.project.functions.__version__", "1.0.0"), \ + patch("runpod.cli.groups.project.functions.copy_template_files"): + create_new_project("test_project", "volume_id", "3.8") + assert mock_open_file.called + assert mock_exists.called + + class TestLaunchProject(unittest.TestCase): """ Test the launch_project function. """