Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to SLIM-CLI #6

Merged
merged 5 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

<hr>

<div align="center">
Expand Down Expand Up @@ -36,6 +37,9 @@ SLIM CLI is a command-line tool designed to infuse SLIM best practices seamlessl
- [Changelog](#changelog)
- [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq)
- [Contributing](#contributing)
- [Local Development](#local-development)
- [Running Tests](#running-tests)
- [Publishing a New Version](#publishing-a-new-version)
- [License](#license)
- [Support](#support)

Expand Down Expand Up @@ -83,6 +87,7 @@ Or select a specific version, such as `X.Y.Z`:
pip install slim-cli==X.Y.Z



### Run Instructions

This section provides detailed commands to interact with the SLIM CLI. Each command includes various options that you can specify to tailor the tool's behavior to your needs.
Expand Down Expand Up @@ -166,7 +171,6 @@ For guidance on how to interact with our team, please see our code of conduct lo

For guidance on our governance approach, including decision-making process and our various roles, please see our governance model at: [GOVERNANCE.md](GOVERNANCE.md)


### Local Development

For local development of SLIM CLI, clone the GitHub repository, create a virtual environment, and then install the package in editable mode into it:
Expand All @@ -180,6 +184,34 @@ pip install --editable .

The `slim` console-script is now ready in editable mode; changes you make to the source files under `src` are immediately reflected when run.

### Running Tests

We use `pytest` for testing. The test files are located within the `tests` subdirectory. To run the tests, ensure you are in the root directory of the project (where the `pyproject.toml` or `setup.py` is located) and have `pytest` installed. You can install `pytest` via pip:

```bash
pip install pytest
```

To execute all tests, simply run:

```bash
pytest
```

If you want to run a specific test file, you can specify the path to the test file:

```bash
pytest tests/jpl/slim/test_cli.py
```

This will run all the tests in the specified file. You can also use `pytest` options like `-v` for verbose output or `-s` to see print statements in the output:

```bash
pytest -v -s
```

### Publishing a New Version

To publish a new version of SLIM CLI to the Python Package Index, typically you'll update the `VERSION.txt` file; then do:
```bash
pip install build wheel twine
Expand All @@ -189,7 +221,6 @@ twine upload dist/*

(Note: this can and should eventually be automated with GitHub Actions.)


## License

See our: [LICENSE](LICENSE)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies = [
'python-dotenv ~= 1.0.1',
'requests ~= 2.32.3',
'rich ~= 13.7.1',
'numpy ~= 2.0.2'

# Commenting-out tabulate since it's not used in the code currenty:
# 'tabulate ~= 0.9.0',
Expand Down
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
pythonpath = src
112 changes: 64 additions & 48 deletions src/jpl/slim/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import argparse
import subprocess
import git
import json
import logging
Expand All @@ -24,6 +25,7 @@
}
GIT_BRANCH_NAME_FOR_MULTIPLE_COMMITS = 'slim-best-practices'
GIT_DEFAULT_REMOTE_NAME = 'origin'
GIT_CUSTOM_REMOTE_NAME = 'slim-custom'
GIT_DEFAULT_COMMIT_MESSAGE = 'SLIM-CLI Best Practices Bot Commit',

def setup_logging():
Expand Down Expand Up @@ -167,6 +169,12 @@ def use_ai(best_practice_id: str, repo_path: str, template_path: str, model: str
reference = fetch_code_base(repo_path)
# Construct the prompt for the AI
prompt = construct_prompt(template_content, best_practice, reference)
elif best_practice_id == 'SLIM-13.1': #readme
reference1 = fetch_readme(repo_path)
reference2 = "\n".join(fetch_relative_file_paths(repo_path))
reference = "EXISTING README:\n" + reference1 + "\n\n" + "EXISTING DIRECTORY LISTING: " + reference2
# Construct the prompt for the AI
prompt = construct_prompt(template_content, best_practice, reference, "Within the provided testing template, only fill out the sections that plausibly have existing tests to fill out based on the directory listing provided (do not make up tests that do not exist).")
else:
reference = fetch_readme(repo_path)
# Construct the prompt for the AI
Expand Down Expand Up @@ -201,17 +209,20 @@ def fetch_code_base(repo_path: str) -> Optional[str]:
code_base += read_file_content(file_path) or ""
return code_base if code_base else None

def construct_prompt(template_content: str, best_practice: Dict[str, Any], reference: str) -> str:
def fetch_relative_file_paths(directory):
relative_paths = []
for root, _, files in os.walk(directory):
for file in files:
# Get the relative file path and add it to the list
relative_path = os.path.relpath(os.path.join(root, file), directory)
relative_paths.append(relative_path)
return relative_paths

def construct_prompt(template_content: str, best_practice: Dict[str, Any], reference: str, comment: str = "") -> str:
return (
f"Fill out all blanks in the template below that start with INSERT. Return the result as Markdown code.\n\n"
##f"Best Practice: {best_practice['title']}\n"
##f"Description: {best_practice['description']}\n\n"
f"Template and output format:\n{template_content}\n\n"
f"Use the info:\n{reference}...\n\n"
f"Show only the updated template output as markdown code."

#f"Use the info:\n{reference}...\n\n"
#f"Generate unit tests"
f"Fill out all blanks in the template below that start with INSERT. Use the provided context information to fill the blanks. Return the template with filled out values. {comment}\n\n"
f"TEMPLATE:\n{template_content}\n\n"
f"CONTEXT INFORMATION:\n{reference}\n\n"
)

def generate_content(prompt: str, model: str) -> Optional[str]:
Expand Down Expand Up @@ -376,6 +387,15 @@ def generate_git_branch_name(best_practice_ids):
return best_practice_ids[0]
else:
return None

def repo_file_to_list(file_path):
# open the file at the given path
with open(file_path, 'r') as file:
# read all lines and strip any leading/trailing whitespace
repos = [line.strip() for line in file.readlines()]
# return the list of lines
return repos


def apply_best_practices(best_practice_ids, use_ai_flag, model, repo_urls = None, existing_repo_dir = None, target_dir_to_clone_to = None):

Expand Down Expand Up @@ -671,19 +691,19 @@ def apply_best_practice(best_practice_id, use_ai_flag, model, repo_url = None, e
logging.error(f"Failed to apply best practice {best_practice_id}")
return None

def deploy_best_practices(best_practice_ids, repo_dir, remote_name = GIT_DEFAULT_REMOTE_NAME, commit_message = GIT_DEFAULT_COMMIT_MESSAGE):
def deploy_best_practices(best_practice_ids, repo_dir, remote = None, commit_message = GIT_DEFAULT_COMMIT_MESSAGE):
# Use shared branch if multiple best_practice_ids else use default branch name
branch_name = generate_git_branch_name(best_practice_ids)

for best_practice_id in best_practice_ids:
deploy_best_practice(
best_practice_id=best_practice_id,
repo_dir=repo_dir,
remote_name=remote_name,
remote=remote,
commit_message=commit_message,
branch=branch_name)

def deploy_best_practice(best_practice_id, repo_dir, remote_name='origin', commit_message='Default commit message', branch=None):
def deploy_best_practice(best_practice_id, repo_dir, remote=None, commit_message='Default commit message', branch=None):
branch_name = branch if branch else best_practice_id

logging.debug(f"Deploying branch: {branch_name}")
Expand All @@ -696,8 +716,19 @@ def deploy_best_practice(best_practice_id, repo_dir, remote_name='origin', commi
repo.git.checkout(branch_name)
logging.debug(f"Checked out to branch {branch_name}")

# Fetch latest info from remote
repo.git.fetch(remote_name)
if remote:
# Use the current GitHub user's default organization as the prefix for the remote
remote_url = f"{remote}/{repo.working_tree_dir.split('/')[-1]}"
remote_name = GIT_CUSTOM_REMOTE_NAME
remote_exists = any(repo_remote.url == remote_url for repo_remote in repo.remotes)
if not remote_exists:
repo.create_remote(remote_name, remote_url)
else:
repo.git.fetch(remote_name)
else:
# Default to using the 'origin' remote
remote_name = GIT_DEFAULT_REMOTE_NAME
repo.git.fetch(remote_name)

# Check if the branch exists on the remote
remote_refs = repo.git.ls_remote('--heads', remote_name, branch_name)
Expand Down Expand Up @@ -736,10 +767,11 @@ def deploy_best_practice(best_practice_id, repo_dir, remote_name='origin', commi
return False


def apply_and_deploy_best_practices(best_practice_ids, use_ai_flag, model, remote_name = GIT_DEFAULT_REMOTE_NAME, commit_message = GIT_DEFAULT_COMMIT_MESSAGE, repo_urls=None, existing_repo_dir=None, target_dir_to_clone_to=None):
def apply_and_deploy_best_practices(best_practice_ids, use_ai_flag, model, remote = None, commit_message = GIT_DEFAULT_COMMIT_MESSAGE, repo_urls=None, existing_repo_dir=None, target_dir_to_clone_to=None):
branch_name = generate_git_branch_name(best_practice_ids)

for repo_url in repo_urls:
git_repo = None # Ensure git_repo is reset for each iteration
if len(best_practice_ids) > 1:
if repo_url:
parsed_url = urllib.parse.urlparse(repo_url)
Expand All @@ -760,7 +792,7 @@ def apply_and_deploy_best_practices(best_practice_ids, use_ai_flag, model, remot
deploy_best_practice(
best_practice_id=best_practice_id,
repo_dir=git_repo.working_tree_dir,
remote_name=remote_name,
remote=remote,
commit_message=commit_message,
branch=branch_name)
else:
Expand All @@ -782,7 +814,7 @@ def apply_and_deploy_best_practices(best_practice_ids, use_ai_flag, model, remot
deploy_best_practice(
best_practice_id=best_practice_id,
repo_dir=git_repo.working_tree_dir,
remote_name=remote_name,
remote=remote,
commit_message=commit_message,
branch=branch_name)
else:
Expand All @@ -796,7 +828,7 @@ def apply_and_deploy_best_practices(best_practice_ids, use_ai_flag, model, remot
deploy_best_practice(
best_practice_id=best_practice_id,
repo_dir=git_repo.working_tree_dir,
remote_name=remote_name,
remote=remote,
commit_message=commit_message,
branch=branch_name)
else:
Expand All @@ -808,36 +840,22 @@ def apply_and_deploy_best_practices(best_practice_ids, use_ai_flag, model, remot
model=model,
repo_url=repo_url,
existing_repo_dir=existing_repo_dir,
target_dir_to_clone_to=branch_name)
target_dir_to_clone_to=target_dir_to_clone_to)

# deploy just the last best practice, which deploys others as well
if git_repo:
deploy_best_practice(
best_practice_id=best_practice_ids[0],
repo_dir=git_repo.working_tree_dir,
remote_name=remote_name,
remote=remote,
commit_message=commit_message,
branch=branch_name)
else:
logging.error(f"Unable to deploy best practice '{best_practice_id}' because apply failed.")
else:
logging.error(f"No best practice IDs specified.")

for best_practice_id in best_practice_ids:
result = apply_and_deploy_best_practice(
best_practice_id,
use_ai_flag,
model,
remote_name,
commit_message,
repo_url,
existing_repo_dir,
target_dir_to_clone_to,
branch=branch_name)
if not result:
logging.error(f"Failed to apply and deploy best practice ID: {best_practice_id}")

def apply_and_deploy_best_practice(best_practice_id, use_ai_flag, model, remote_name=GIT_DEFAULT_REMOTE_NAME, commit_message=GIT_DEFAULT_COMMIT_MESSAGE, repo_url = None, existing_repo_dir = None, target_dir_to_clone_to = None, branch = None):
def apply_and_deploy_best_practice(best_practice_id, use_ai_flag, model, remote=None, commit_message=GIT_DEFAULT_COMMIT_MESSAGE, repo_url = None, existing_repo_dir = None, target_dir_to_clone_to = None, branch = None):
logging.debug("AI customization enabled for applying and deploying best practices" if use_ai_flag else "AI customization disabled")
logging.debug(f"Applying and deploying best practice ID: {best_practice_id}")

Expand All @@ -846,7 +864,7 @@ def apply_and_deploy_best_practice(best_practice_id, use_ai_flag, model, remote_

# Deploy the best practice if applied successfully
if git_repo:
result = deploy_best_practice(best_practice_id=best_practice_id, repo_dir=git_repo.working_tree_dir, remote_name=remote_name, commit_message=commit_message, branch=branch)
result = deploy_best_practice(best_practice_id=best_practice_id, repo_dir=git_repo.working_tree_dir, remote=remote, commit_message=commit_message, branch=branch)
if result:
logging.info(f"Successfully applied and deployed best practice ID: {best_practice_id}")
return True
Expand All @@ -871,16 +889,15 @@ def create_parser():
parser_apply = subparsers.add_parser('apply', help='Applies a best practice, i.e. places a best practice in a git repo in the right spot with appropriate content')
parser_apply.add_argument('--best-practice-ids', nargs='+', required=True, help='Best practice IDs to apply')
parser_apply.add_argument('--repo-urls', nargs='+', required=False, help='Repository URLs to apply to. Do not use if --repo-dir specified')
parser_apply.add_argument('--repo-urls-file', required=False, help='Path to a file containing repository URLs')
parser_apply.add_argument('--repo-dir', required=False, help='Repository directory location on local machine. Only one repository supported')
parser_apply.add_argument('--clone-to_dir', required=False, help='Local path to clone repository to. Compatible with --repo-urls')
#parser_apply.add_argument('--use-ai', action='store_true', help='Automatically customize the application of the best practice')
parser_apply.add_argument('--clone-to-dir', required=False, help='Local path to clone repository to. Compatible with --repo-urls')
parser_apply.add_argument('--use-ai', metavar='MODEL', help=f"Automatically customize the application of the best practice with an AI model. Support for: {get_ai_model_pairs(SUPPORTED_MODELS)}")
#parser_apply.add_argument('--model', required=False, help='Model name (openai/gpt-4o) for using ai')
parser_apply.set_defaults(func=lambda args: apply_best_practices(
best_practice_ids=args.best_practice_ids,
use_ai_flag=bool(args.use_ai),
model=args.use_ai if args.use_ai else None,
repo_urls=args.repo_urls,
repo_urls=repo_file_to_list(args.repo_urls_file) if args.repo_urls_file else args.repo_urls,
existing_repo_dir=args.repo_dir,
target_dir_to_clone_to=args.clone_to_dir
))
Expand All @@ -889,33 +906,32 @@ def create_parser():
parser_deploy = subparsers.add_parser('deploy', help='Deploys a best practice, i.e. places the best practice in a git repo, adds, commits, and pushes to the git remote.')
parser_deploy.add_argument('--best-practice-ids', nargs='+', required=True, help='Best practice IDs to deploy')
parser_deploy.add_argument('--repo-dir', required=False, help='Repository directory location on local machine')
parser_deploy.add_argument('--remote-name', required=False, default=GIT_DEFAULT_REMOTE_NAME, help=f"Name of the remote to push changes to. Default: '{GIT_DEFAULT_REMOTE_NAME}")
parser_deploy.add_argument('--remote', required=False, default=None, help=f"Push to a specified remote. If not specified, pushes to '{GIT_DEFAULT_REMOTE_NAME}")
parser_deploy.add_argument('--commit-message', required=False, default=GIT_DEFAULT_COMMIT_MESSAGE, help=f"Commit message to use for the deployment. Default '{GIT_DEFAULT_COMMIT_MESSAGE}")
parser_deploy.set_defaults(func=lambda args: deploy_best_practices(
best_practice_ids=args.best_practice_ids,
repo_dir=args.repo_dir,
remote_name=args.remote_name,
remote=args.remote,
commit_message=args.commit_message
))

# Parser for applying and deploying a best practice together
parser_apply_deploy = subparsers.add_parser('apply-deploy', help='Applies and deploys a best practice')
parser_apply_deploy.add_argument('--best-practice-ids', nargs='+', required=True, help='Best practice IDs to apply')
parser_apply_deploy.add_argument('--repo-urls', nargs='+', required=False, help='Repository URLs to apply to. Do not use if --repo-dir specified')
parser_apply_deploy.add_argument('--repo-urls-file', required=False, help='Path to a file containing repository URLs')
parser_apply_deploy.add_argument('--repo-dir', required=False, help='Repository directory location on local machine. Only one repository supported')
parser_apply_deploy.add_argument('--clone-to-dir', required=False, help='Local path to clone repository to. Compatible with --repo-urls')
parser_apply_deploy.add_argument('--use-ai', metavar='MODEL', help='Automatically customize the application of the best practice with the specified AI model. Support for: {get_ai_model_pairs(SUPPORTED_MODELS)}')
#parser_apply_deploy.add_argument('--use-ai', action='store_true', help='Automatically customize the application of the best practice')
#parser_apply_deploy.add_argument('--model', required=False, help='Model name (ollama/gpt-4o) for using ai')
parser_apply_deploy.add_argument('--remote-name', required=False, default=GIT_DEFAULT_REMOTE_NAME, help=f"Name of the remote to push changes to. Default: '{GIT_DEFAULT_REMOTE_NAME}")
parser_apply_deploy.add_argument('--remote', required=False, default=None, help=f"Push to a specified remote. If not specified, pushes to '{GIT_DEFAULT_REMOTE_NAME}")
parser_apply_deploy.add_argument('--commit-message', required=False, default=GIT_DEFAULT_COMMIT_MESSAGE, help=f"Commit message to use for the deployment. Default '{GIT_DEFAULT_COMMIT_MESSAGE}")
parser_apply_deploy.set_defaults(func=lambda args: apply_and_deploy_best_practices(
best_practice_ids=args.best_practice_ids,
use_ai_flag=bool(args.use_ai),
model=args.use_ai if args.use_ai else None,
remote_name=args.remote_name,
remote=args.remote,
commit_message=args.commit_message,
repo_urls=args.repo_urls,
repo_urls=repo_file_to_list(args.repo_urls_file) if args.repo_urls_file else args.repo_urls,
existing_repo_dir=args.repo_dir,
target_dir_to_clone_to=args.clone_to_dir
))
Expand Down
Loading