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

rippling cli: Added flux build init command #7

Merged
merged 6 commits into from
Apr 24, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

# rippling cli
.rippling_cli/
vguptarippling marked this conversation as resolved.
Show resolved Hide resolved
16 changes: 7 additions & 9 deletions rippling_cli/cli/commands/flux/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rippling_cli.config.config import get_app_config, save_app_config
from rippling_cli.constants import RIPPLING_API
from rippling_cli.core.api_client import APIClient
from rippling_cli.utils.app_utils import get_app_from_id
from rippling_cli.utils.login_utils import ensure_logged_in


Expand Down Expand Up @@ -38,20 +39,17 @@ def list() -> None:
def set(app_id: str) -> None:
"""This command sets the current app within the app_config.json file located in the .rippling directory."""
ctx: click.Context = click.get_current_context()
api_client = APIClient(base_url=RIPPLING_API, headers={"Authorization": f"Bearer {ctx.obj.oauth_token}"})

endpoint = "/apps/api/apps/?large_get_query=true"
response = api_client.post(endpoint, data={"query": f"id={app_id}&limit=1"})
app_list = response.json() if response.status_code == 200 else []
app_json = get_app_from_id(app_id, ctx.obj.oauth_token)

if response.status_code != 200 or len(app_list) == 0:
if not app_json:
click.echo(f"Invalid app id: {app_id}")
return

app_name = app_list[0].get("displayName")
display_name = app_json.get("displayName")
app_name = app_json.get("name")

save_app_config(app_id, app_name)
click.echo(f"Current app set to {app_name} ({app_id})")
save_app_config(app_id, display_name, app_name)
click.echo(f"Current app set to {display_name} ({app_id})")


@app.command()
Expand Down
51 changes: 51 additions & 0 deletions rippling_cli/cli/commands/flux/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import click

from rippling_cli.config.config import get_app_config
from rippling_cli.core.setup_project import setup_project
from rippling_cli.utils.app_utils import get_starter_package_for_app
from rippling_cli.utils.build_utils import (
remove_existing_starter_package,
starter_package_already_extracted_on_current_directory,
)
from rippling_cli.utils.file_utils import download_file_using_url, extract_zip_to_current_cwd
from rippling_cli.utils.login_utils import ensure_logged_in, get_current_role_name_and_email


@click.group()
@click.pass_context
def build(ctx: click.Context) -> None:
"""Manage flux builds"""
ensure_logged_in(ctx)


@build.command()
def init() -> None:
"""This command downloads the starter package and extracts it into a specified folder."""
ctx: click.Context = click.get_current_context()

if starter_package_already_extracted_on_current_directory():
if not click.confirm("Starter package already extracted. Do you want to replace?"):
return
remove_existing_starter_package()

# get the starter package for the app
download_url: str = get_starter_package_for_app(ctx.obj.oauth_token)
if not download_url:
click.echo("No starter package found.")
return
app_config = get_app_config()
app_display_name = app_config.get('displayName')
filename = app_display_name + ".zip"
# download the starter package
is_file_downloaded = download_file_using_url(download_url, filename)
vguptarippling marked this conversation as resolved.
Show resolved Hide resolved
if not is_file_downloaded:
click.echo("Failed to download the starter package.")
return

# extract the starter package
extract_zip_to_current_cwd(filename)

name, email = get_current_role_name_and_email(ctx.obj.oauth_token)

# setup the project
setup_project(name, email)
2 changes: 2 additions & 0 deletions rippling_cli/cli/commands/flux/flux.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import click

from rippling_cli.cli.commands.flux.app import app
from rippling_cli.cli.commands.flux.build import build
from rippling_cli.utils.login_utils import ensure_logged_in


Expand All @@ -12,3 +13,4 @@ def flux(ctx: click.Context) -> None:


flux.add_command(app) # type: ignore
flux.add_command(build) # type: ignore
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[best-practice]. Untyped 'type: ignore' or specific typed 'type: ignore' comments like 'attr-defined', 'method-assign', 'name-defined', 'operator' should be avoided as they can hide real issues.

💬 Reply with /semgrep ignore <reason> or triage in Semgrep Cloud Platform to ignore the finding created by development_avoid_type_ignore_error.

5 changes: 3 additions & 2 deletions rippling_cli/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def get_app_config():
return {}


def save_app_config(app_id: str, app_name: str):
def save_app_config(app_id: str, display_name: str, app_name: str):
"""
Save the app configuration to the specified directory.
Args:
Expand All @@ -95,7 +95,8 @@ def save_app_config(app_id: str, app_name: str):

app_config = {
"id": app_id,
"displayName": app_name
"displayName": display_name,
"name": app_name,
}

config_file = config_dir / APP_CONFIG_FILE
Expand Down
10 changes: 5 additions & 5 deletions rippling_cli/core/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ def __init__(self, base_url, headers=None):
self.base_url = base_url
self.headers = headers or {}

def make_request(self, method, endpoint, params=None, data=None):
def make_request(self, method, endpoint, params=None, data=None, stream=False):
url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
response = requests.request(method, url, params=params, json=data, headers=self.headers)
response = requests.request(method, url, params=params, json=data, headers=self.headers, stream=stream)
return response

def get(self, endpoint, params=None):
return self.make_request("GET", endpoint, params=params)
def get(self, endpoint, params=None, stream=False):
return self.make_request("GET", endpoint, params=params, stream=stream)

def post(self, endpoint, data):
def post(self, endpoint, data=None):
return self.make_request("POST", endpoint, data=data)

def put(self, endpoint, data):
Expand Down
113 changes: 113 additions & 0 deletions rippling_cli/core/setup_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import os
import subprocess
import sys

import click

from rippling_cli.exceptions.build_exceptions import PythonCreationFailed
from rippling_cli.utils.build_utils import create_pyproject_toml, get_run_config_xml_content
from rippling_cli.utils.file_utils import create_directory_inside_path


# TODO: Since run configuration cannot be transferred from import/export settings , it lies inside .idea folder in \
# the project directory. This should be a separate command \
# https://intellij-support.jetbrains.com/hc/en-us/community/posts/206600965-Export-Import-Run-Configurations
def create_run_configurations(project_name: str):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is this function called?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of initially including this part of command but looks like for pycharm run/debug configuration cannot be transferred so we will have to create .idea folder and copy the xml file inside the folder. I am planning to add this as part of run_server command as I am hoping that app-developer would have opened the project in pycharm. Since adding this file before in this command creates problem with pycharm settings which they pycharm does on project open

# Create the .idea directory if it doesn't exist
create_directory_inside_path(os.getcwd(), ".idea")

# Create the runConfigurations directory inside .idea
create_directory_inside_path(f"{os.getcwd()}/.idea", "runConfigurations")

# Create the Flask__flux_dev_tools_server_flask_.xml file
xml_file_path = os.path.join(f"{os.getcwd()}/.idea/runConfigurations", "Flask__flux_dev_tools_server_flask_.xml")
with open(xml_file_path, "w") as xml_file:
xml_file.write(get_run_config_xml_content(project_name))


def setup_project(name=None, email=None):
# Check if pip is available
try:
subprocess.check_output(["pip", "--version"])
except FileNotFoundError:
click.echo("pip is not installed. Installing Python and pip...")

# Install Python
install_python()
vguptarippling marked this conversation as resolved.
Show resolved Hide resolved

# Check if Python installation was successful
try:
subprocess.check_output(["python", "--version"])
except FileNotFoundError:
click.echo("Python installation failed. Please install Python manually.")
return

# Install pip using ensurepip
subprocess.run([sys.executable, "-m", "ensurepip", "--default-pip"])
click.echo("pip has been installed.")

# Check if Poetry is installed
try:
subprocess.check_output(["poetry", "--version"])
click.echo("Poetry is already installed.")
except FileNotFoundError:
click.echo("Poetry is not installed. Installing Poetry...")

# Install Poetry using pip
subprocess.run(["pip", "install", "poetry"])
click.echo("Poetry has been installed.")

# Check if Poetry is installed successfully
try:
subprocess.check_output(["poetry", "--version"])
except FileNotFoundError:
click.echo("Poetry installation failed. Please install Poetry manually.")
return

# Check if pyproject.toml already exists
if os.path.exists("pyproject.toml"):
click.echo("pyproject.toml already exists. Aborting setup.")
return

authors = "developer <[email protected]>"
if name and email:
authors = f"{name} <{email}>" # Use provided name and email

# Get the current working directory
current_directory = os.getcwd()

project_name = os.path.basename(current_directory) # Default project name
project_name.replace("-", "_")

# Create pyproject.toml file with default content
create_pyproject_toml(project_name, authors)

# Install dependencies
click.echo("Installing dependencies...")
subprocess.run(["poetry", "install"])
click.echo("Dependencies installed.")


def install_python():
if sys.platform == "darwin": # Check if macOS
# Try installing Python using Homebrew
try:
subprocess.run(["brew", "install", "python"])
return
except FileNotFoundError:
pass # Homebrew not available, fall back to manual installation

# Prompt the user to install Python manually from python.org
click.echo("Python is not installed. Please install Python manually from https://www.python.org/downloads/")
raise PythonCreationFailed()
elif sys.platform.startswith("linux"):
# Install Python on Linux using package manager (e.g., apt)
subprocess.run(["sudo", "apt", "update"])
subprocess.run(["sudo", "apt", "install", "python3"])
elif sys.platform.startswith("win"):
# Install Python on Windows using python.org installer
click.echo("Please install Python manually from https://www.python.org/downloads/")
raise PythonCreationFailed()
else:
click.echo("Unsupported platform. Please install Python manually.")
raise PythonCreationFailed()
Empty file.
10 changes: 10 additions & 0 deletions rippling_cli/exceptions/build_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class PythonCreationFailed(Exception):
def __init__(self, message="Failed to install Python. Please install Python manually."):
self.message = message
super().__init__(self.message)


class DirectoryCreationFailed(Exception):
def __init__(self, message="Failed to create directory."):
self.message = message
super().__init__(self.message)
24 changes: 24 additions & 0 deletions rippling_cli/utils/app_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from rippling_cli.config.config import get_app_config
from rippling_cli.constants import RIPPLING_API
from rippling_cli.core.api_client import APIClient


def get_app_from_id(app_id, oauth_token):
api_client = APIClient(base_url=RIPPLING_API, headers={"Authorization": f"Bearer {oauth_token}"})

endpoint = "/apps/api/apps/?large_get_query=true"
response = api_client.post(endpoint, data={"query": f"id={app_id}&limit=1"})
app_list = response.json() if response.status_code == 200 else []

if response.status_code != 200 or len(app_list) == 0:
return None
return app_list[0]


def get_starter_package_for_app(oauth_token):
api_client = APIClient(base_url=RIPPLING_API, headers={"Authorization": f"Bearer {oauth_token}"})
app_config = get_app_config()
endpoint = f"/apps/api/flux_apps/get_starter_package?app_name={app_config.get('name')}"
response = api_client.post(endpoint)
response.raise_for_status()
return response.json().get("link")
Loading
Loading