Skip to content

Commit

Permalink
rippling cli: Added flux build init command (#7)
Browse files Browse the repository at this point in the history
* Added rippling flux build init command

* added comment

* fixed import

* fixed typing

* review comment changes

* import fixes
  • Loading branch information
vguptarippling authored Apr 24, 2024
1 parent 3625a95 commit 4732bd6
Show file tree
Hide file tree
Showing 13 changed files with 417 additions and 16 deletions.
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/
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)
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
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
124 changes: 124 additions & 0 deletions rippling_cli/core/setup_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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):
# 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 check_python_installed():
try:
subprocess.check_output(["python", "--version"])
return True
except FileNotFoundError:
return False


def install_pip():
subprocess.run([sys.executable, "-m", "ensurepip", "--default-pip"])
click.echo("pip has been installed.")


def check_pip_installed():
try:
subprocess.check_output(["pip", "--version"])
except FileNotFoundError:
click.echo("pip is not installed. Installing pip...")
install_pip()


def install_poetry():
subprocess.run(["pip", "install", "poetry"])
click.echo("Poetry has been installed.")


def check_poetry_installed():
try:
subprocess.check_output(["poetry", "--version"])
return True
except FileNotFoundError:
return False


def setup_project(name=None, email=None):
# Check if Python is already installed
if not check_python_installed():
click.echo("Python is not installed. Installing Python...")
install_python()
if not check_python_installed():
click.echo("Python installation failed. Please install Python manually.")
return

# Check if pip is installed
check_pip_installed()

# Check if Poetry is installed
if not check_poetry_installed():
click.echo("Poetry is not installed. Installing Poetry...")
install_poetry()
if not check_poetry_installed():
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

project_name = "app" # Default project name

# 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

0 comments on commit 4732bd6

Please sign in to comment.