From 07e796c1ca55955bd2b7b0872fb326e9f2690db5 Mon Sep 17 00:00:00 2001 From: Rick Chen Date: Tue, 1 Oct 2024 22:27:56 -0700 Subject: [PATCH 1/7] impl --- .ci-pre-commit-config.yaml | 4 +- .gitignore | 1 + cased/cli.py | 4 +- cased/commands/build.py | 9 +- cased/commands/deploy.py | 17 +- cased/commands/init.py | 2 + cased/commands/login.py | 167 ++++++++----------- cased/commands/resources.py | 311 ++++++++++++++++++++++++++++++++---- cased/utils/api.py | 168 +++++++++++-------- cased/utils/auth.py | 73 ++++++--- cased/utils/config.py | 32 ++++ cased/utils/constants.py | 22 +++ cased/utils/progress.py | 16 +- poetry.lock | 204 +++++++++++------------ pyproject.toml | 2 +- 15 files changed, 669 insertions(+), 363 deletions(-) create mode 100644 cased/utils/config.py create mode 100644 cased/utils/constants.py diff --git a/.ci-pre-commit-config.yaml b/.ci-pre-commit-config.yaml index 9797595..63cdd73 100644 --- a/.ci-pre-commit-config.yaml +++ b/.ci-pre-commit-config.yaml @@ -4,16 +4,14 @@ repos: hooks: - id: end-of-file-fixer - id: trailing-whitespace - exclude: "^comet/core/migrations/.*" - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.0.291 hooks: - id: ruff - exclude: "^comet/core/migrations/.*" + args: [--line-length=100] - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort - exclude: "^comet/core/migrations/.*" diff --git a/.gitignore b/.gitignore index e1cbebc..2c34b3d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ env/ venv/ .DS_STORE .pre-commit-config.yaml +.pem diff --git a/cased/cli.py b/cased/cli.py index ec7c2b4..fa94558 100644 --- a/cased/cli.py +++ b/cased/cli.py @@ -4,7 +4,7 @@ from cased.commands.deploy import deploy from cased.commands.init import init from cased.commands.login import login, logout -from cased.commands.resources import branches, deployments +from cased.commands.resources import branches, deployments, projects, targets CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -27,6 +27,8 @@ def cli(): cli.add_command(logout) cli.add_command(deployments) cli.add_command(branches) +cli.add_command(projects) +cli.add_command(targets) # ... (keep the login and setup_target commands as they were) ... diff --git a/cased/commands/build.py b/cased/commands/build.py index 9adf411..ca4e94f 100644 --- a/cased/commands/build.py +++ b/cased/commands/build.py @@ -10,7 +10,8 @@ from rich.panel import Panel from rich.progress import Progress, SpinnerColumn, TextColumn -from cased.utils.api import API_BASE_URL, create_secrets +from cased.utils.api import CasedAPI +from cased.utils.constants import CasedConstants from cased.utils.git import get_repo_name console = Console() @@ -73,7 +74,7 @@ def build() -> None: project_name = get_repo_name() secrets = extract_secrets_from_workflow(workflow_content) - create_secrets(project_name, secrets) + CasedAPI().create_secrets(project_name, secrets) console.print( Panel( @@ -81,9 +82,9 @@ def build() -> None: [bold green]GitHub Actions workflow generated successfully![/bold green] [bold green]Please complete the following steps for the workflow to work correctly: [/bold green] 1. Review the generated workflow file in .github/workflows/deploy.yaml - 2. Go to {API_BASE_URL}/secrets/{project_name} to update the secrets. + 2. Go to {CasedConstants.API_BASE_URL}/secrets/{project_name} to update the secrets. 3. Commit the changes to your repository, and the workflow will be triggered. - 4. Go to {API_BASE_URL}/deployments/ to monitor the deployment status. + 4. Go to {CasedConstants.API_BASE_URL}/deployments/ to monitor the deployment status. """, # noqa: E501 title="Success", expand=False, diff --git a/cased/commands/deploy.py b/cased/commands/deploy.py index 11fe7fa..3f76ad0 100644 --- a/cased/commands/deploy.py +++ b/cased/commands/deploy.py @@ -2,15 +2,18 @@ import questionary from rich.console import Console -from cased.utils.api import deploy_branch, get_branches -from cased.utils.auth import get_token +from cased.utils.api import CasedAPI +from cased.utils.auth import validate_credentials from cased.utils.progress import run_process_with_status_bar console = Console() def _build_questionary_choices(): - data = run_process_with_status_bar(get_branches, "Fetching branches...", timeout=10) + api_client = CasedAPI() + data = run_process_with_status_bar( + api_client.get_branches, "Fetching branches...", timeout=10 + ) branches = data.get("pull_requests", []) deployable_branches = [ branch for branch in branches if branch["deployable"] is True @@ -51,6 +54,7 @@ def _build_questionary_choices(): @click.command() @click.option("--branch", help="Branch to deploy") @click.option("--target", help="Target environment for deployment") +@validate_credentials(check_project_set=False) def deploy(branch, target): """ Deploy a branch to a target environment. @@ -65,11 +69,6 @@ def deploy(branch, target): cased deploy cased deploy --branch feature-branch-1 --target dev """ # noqa: E501 - token = get_token() - if not token: - console.print("[red]Please log in first using 'cased login'[/red]") - return - if not branch and not target: branch, target = _build_questionary_choices() @@ -78,7 +77,7 @@ def deploy(branch, target): ) if branch and target: - deploy_branch(branch, target) + CasedAPI().deploy_branch(branch, target) console.print("[green]Dispatch succeeded. Starting deployment...[/green]") else: console.print("[red]Deployment dispatch failed. Please try again later.[/red]") diff --git a/cased/commands/init.py b/cased/commands/init.py index bdbedb2..63fd074 100644 --- a/cased/commands/init.py +++ b/cased/commands/init.py @@ -6,12 +6,14 @@ from rich.console import Console from rich.panel import Panel +from cased.utils.auth import validate_credentials from cased.utils.progress import run_process_with_status_bar console = Console() @click.command() +@validate_credentials(check_project_set=False) def init(): """Initialize a new project configuration.""" console.print(Panel.fit("Welcome to Cased", style="bold magenta")) diff --git a/cased/commands/login.py b/cased/commands/login.py index 1e89116..efa6a39 100644 --- a/cased/commands/login.py +++ b/cased/commands/login.py @@ -1,56 +1,18 @@ -import json -import os -import random -import stat -import tempfile -import time -from datetime import datetime, timedelta - import click from rich.console import Console from rich.panel import Panel from rich.progress import Progress +from rich.prompt import Prompt -from cased.utils.auth import CONFIG_DIR, TOKEN_FILE +from cased.commands.resources import projects +from cased.utils.api import validate_tokens +from cased.utils.auth import validate_credentials +from cased.utils.config import delete_config, save_config +from cased.utils.constants import CasedConstants console = Console() -def ensure_config_dir(): - # Create dir with restricted permissions if it doesn't exist - os.makedirs(CONFIG_DIR, mode=0o700, exist_ok=True) - - -def secure_write(file_path, data): - ensure_config_dir() - # Write data to a file with restricted permissions - with open(file_path, "w") as f: - json.dump(data, f) - os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR) # Read/write only for the owner - - -def create_temp_token_file(token_data): - # Create a temporary file that will be automatically deleted after 24 hours - temp_dir = tempfile.gettempdir() - temp_file = tempfile.NamedTemporaryFile( - mode="w+", delete=False, dir=temp_dir, prefix="cased_token_", suffix=".json" - ) - - with temp_file: - json.dump(token_data, temp_file) - - os.chmod( - temp_file.name, stat.S_IRUSR | stat.S_IWUSR - ) # Read/write only for the owner - - # Schedule file for deletion after 24 hours - deletion_time = datetime.now() + timedelta(hours=24) - deletion_command = f"(sleep {(deletion_time - datetime.now()).total_seconds()} && rm -f {temp_file.name}) &" # noqa: E501 - os.system(deletion_command) - - return temp_file.name - - @click.command() def login(): """ @@ -59,69 +21,78 @@ def login(): This command initiates a login process, stores a session token, and provides information about the session expiration. """ - with Progress() as progress: - task1 = progress.add_task("[green]Initiating handshake...", total=100) - task2 = progress.add_task("[yellow]Authenticating...", total=100) - - while not progress.finished: - progress.update(task1, advance=100) - time.sleep(0.25) - progress.update(task2, advance=15) - - # Simulate getting a session token - session_token = "fake_session_token_" + str(random.randint(1000, 9999)) - expiry = datetime.now() + timedelta(hours=24) # Token expires in 1 hour - - token_data = {"token": session_token, "expiry": expiry.isoformat()} - - # Create a temporary token file - temp_token_file = create_temp_token_file(token_data) + console.print(Panel("Welcome to Cased CLI", style="bold blue")) - # Store the path to the temporary file - secure_write(TOKEN_FILE, {"temp_file": temp_token_file}) + org_name = Prompt.ask("Enter your organization name") + api_key = Prompt.ask("Enter your API key", password=True) - # Calculate time until expiration - time_until_expiry = expiry - datetime.now() - hours, remainder = divmod(time_until_expiry.seconds, 3600) - minutes, _ = divmod(remainder, 60) - - console.print( - Panel( - f"[green]Login successful![/green]\nSession token stored securely.\nSession expires in {hours} hours {minutes} minutes.", # noqa: E501 - title="Login Status", + with Progress() as progress: + task = progress.add_task("[cyan]Validating credentials...", total=100) + + # Simulate API call with progress + for i in range(0, 101, 10): + progress.update(task, advance=10) + if i == 50: + response = validate_tokens(api_key, org_name) + progress.update(task, completed=100) + + # 200 would mean success, + # 403 would mean validation success but necessary integration is not set up. + # (E.g. Github) + if response.status_code == 200 or response.status_code == 403: + data = response.json() + elif response.status_code == 401: + console.print( + Panel( + f"[bold red]Unauthorized:[/bold red] Invalid API token. Please try again or check your API token at {CasedConstants.BASE_URL}/settings/", # noqa: E501 + expand=False, + ) ) - ) - - -@click.command() -def logout(): - """ - Log out from the Cased system. - - This command removes the stored session token and ends the current session. - """ - if os.path.exists(TOKEN_FILE): - with open(TOKEN_FILE, "r") as f: - data = json.load(f) - temp_file = data.get("temp_file") - - # Remove the temporary token file if it exists - if temp_file and os.path.exists(temp_file): - os.remove(temp_file) - - # Remove the token file in the config directory - os.remove(TOKEN_FILE) - + return + elif response.status_code == 404: console.print( Panel( - "[green]Logout successful![/green]\nSession token has been removed.", - title="Logout Status", + f"[bold red]Organization not found:[/bold red] Please check your organization name at {CasedConstants.BASE_URL}/settings/", # noqa: E501 + expand=False, ) ) + return + else: + click.echo("Sorry, something went wrong. Please try again later.") + return + + if data.get("validation"): + org_id = data.get("org_id") + data = { + CasedConstants.CASED_API_AUTH_KEY: api_key, + CasedConstants.CASED_ORG_ID: org_id, + CasedConstants.CASED_ORG_NAME: org_name, + } + save_config(data) + console.print(Panel("[bold green]Login successful![/bold green]", expand=False)) + # Ask user to select a project. + ctx = click.get_current_context() + ctx.invoke(projects, details=False) else: console.print( Panel( - "[yellow]No active session found.[/yellow]\nYou are already logged out.", # noqa: E501 - title="Logout Status", + f"[bold red]Login failed:[/bold red] {data.get('reason', 'Unknown error')}", + title="Error", + expand=False, ) ) + + +@click.command() +@validate_credentials +def logout(): + """ + Log out from your Cased account. + + This command removes all locally stored credentials, + effectively logging you out of the Cased CLI. + """ + delete_config() + console.print( + Panel("[bold green]Logged out successfully![/bold green]", expand=False) + ) diff --git a/cased/commands/resources.py b/cased/commands/resources.py index f02a315..c871153 100644 --- a/cased/commands/resources.py +++ b/cased/commands/resources.py @@ -1,19 +1,196 @@ +""" +Cased CLI Tool + +This script provides a command-line interface for interacting with the Cased API. +It offers functionality to manage projects, view deployments, list targets, and display active branches. + +The tool uses the Click library for creating CLI commands and the Rich library for enhanced console output. +It also utilizes questionary for interactive prompts and the Cased API client for data retrieval. + +Commands: + projects: Display and select Cased projects. + deployments: Show recent deployments for a selected project and target. + targets: List target environments for a selected project. + branches: Display active branches with various details. + +Each command supports different options for customizing the output and filtering results. + +Usage: + cased [COMMAND] [OPTIONS] + +For detailed usage of each command, use the --help option: + cased [COMMAND] --help + +Dependencies: + - click + - questionary + - rich + - dateutil + +Author: Cased +Date: 10/01/2024 +Version: 1.0 +""" # noqa: E501 + import click +import questionary from dateutil import parser +from questionary import Style +from rich import box from rich.console import Console +from rich.panel import Panel from rich.table import Table from rich.text import Text -from cased.utils.api import get_branches, get_deployments -from cased.utils.auth import get_token +from cased.utils.api import CasedAPI +from cased.utils.auth import validate_credentials +from cased.utils.config import load_config, save_config +from cased.utils.constants import CasedConstants from cased.utils.progress import run_process_with_status_bar console = Console() +# Custom style for questionary +custom_style = Style( + [ + ("qmark", "fg:#673ab7 bold"), # token in front of the question + ("question", "bold"), # question text + ("answer", "fg:#f44336 bold"), # submitted answer text behind the question + ("pointer", "fg:#673ab7 bold"), # pointer used in select and checkbox prompts + ( + "highlighted", + "fg:#673ab7 bold", + ), # pointed-at choice in select and checkbox prompts + ("selected", "fg:#cc5454"), # style for a selected item of a checkbox + ("separator", "fg:#cc5454"), # separator in lists + ("instruction", ""), # user instructions for select, rawselect, checkbox + ("text", ""), # plain text + ( + "disabled", + "fg:#858585 italic", + ), # disabled choices for select and checkbox prompts + ] +) + + +@click.command() +@click.option( + "--details", + "-d", + is_flag=True, + default=True, + help="Show detailed information about projects", +) +@validate_credentials +def projects(details=True): + """ + Display and select Cased projects. + + This command shows a list of available projects and allows you to select one as your current working project. + If a project is already selected, it will be highlighted in the list. + The selected project's name and ID are stored as environment variables for future use. + """ # noqa: E501 + # Check if a project is already selected + config = load_config() + current_project_name = config.get(CasedConstants.CASED_WORKING_PROJECT_NAME) + current_project_id = config.get(CasedConstants.CASED_WORKING_PROJECT_ID) + + raw_projects = CasedAPI().get_projects() + projects = [ + { + "id": project["id"], + "repository_full_name": project["repository_full_name"], + "code_host": project["code_host"], + "latest_deployment": ( + project["latest_deployment"].get("branch") + if project["latest_deployment"] + else "N/A" + ), + } + for project in raw_projects["projects"] + ] + + if details: + # Create a table to display projects + table = Table(title="Projects Details", box=box.ROUNDED) + table.add_column("ID", style="cyan", no_wrap=True) + table.add_column("Repository", style="magenta") + table.add_column("Code Host", style="green") + table.add_column("Latest Deployment", style="yellow") + + for project in projects: + row_style = "bold" if str(project["id"]) == current_project_id else "" + table.add_row( + str(project["id"]), + project["repository_full_name"], + project["code_host"], + project["latest_deployment"], + style=row_style, + ) + + console.print(table) + + if current_project_name and current_project_id: + console.print( + f"[bold green]Currently working on:[/bold green] {current_project_name}" + ) + console.print() + else: + console.print( + "[yellow]No project selected. Please select a project from the list below:[/yellow]" + ) + # Prepare choices for questionary + choices = ["Exit without changing project"] + choices.extend( + [ + f"{project['id']} - {project['repository_full_name']} ({project['code_host']})" + for project in projects + ] + ) + + # Prompt user for selection using questionary + try: + selection = questionary.select( + "Select a project:", choices=choices, style=custom_style + ).ask() + except KeyboardInterrupt: + console.print("[yellow]Exiting without changing project.[/yellow]") + return + + if not selection: + console.print("[yellow]No project selected. [/yellow]") + return + + if selection == "Exit without changing project": + console.print("[yellow]No updates.[/yellow]") + return + + # Extract project ID from selection + selected_id = int(selection.split(" - ")[0]) + + # Update environment variables + selected_project = next(p for p in projects if p["id"] == selected_id) + selected_data = { + CasedConstants.CASED_WORKING_PROJECT_NAME: selected_project[ + "repository_full_name" + ], + CasedConstants.CASED_WORKING_PROJECT_ID: str(selected_id), + } + save_config(selected_data) + + console.print( + Panel( + f"[bold green]Project updated:[/bold green] {selected_project['repository_full_name']}" + ) + ) + @click.command() @click.option("--limit", default=5, help="Number of deployments to show") -def deployments(limit): +@click.option("--project", default="", help="Project name to filter branches") +@click.option("--target", default="", help="Target name to filter branches") +@validate_credentials(check_project_set=True) +def deployments(limit, project, target): """ Display recent deployments. @@ -22,22 +199,30 @@ def deployments(limit): Use the --limit option to specify the number of deployments to display. """ - token = get_token() - if not token: - console.print("[red]Please log in first using 'cased login'[/red]") - return + if not target: + targets = CasedAPI().get_targets(project).get("targets", []) + if not targets: + console.print( + f"[yellow]No targets available. You can add a target by going to {CasedConstants.BASE_URL}deployments/{project}/targets/new[/yellow]" # noqa: E501 + ) + return - table = Table(title="Recent Deployments") + selection = questionary.select( + "Select a project:", choices=targets, style=custom_style + ).ask() + if not selection: + console.print("[yellow]No target selected.[/yellow]") + return + target = selection - table.add_column("Begin Time", style="cyan") - table.add_column("End Time", style="cyan") - table.add_column("Deployer", style="magenta") - table.add_column("Status", style="green") - table.add_column("Branch", style="yellow") - table.add_column("Target", style="blue") - table.add_column("View", style="cyan") - - data = get_deployments().get("deployments", []) + data = ( + CasedAPI() + .get_deployments(project_name=project, target_name=target) + .get("deployments", []) + ) + if not data: + console.print("[red]No deployments available.[/red]") + return deployments_data = [] for idx, deployment in enumerate(data): @@ -51,7 +236,7 @@ def deployments(limit): ) status = deployment.get("status", "Unknown") deployment_id = deployment.get("id") - view_url = f"https://cased.com/deployments/{deployment_id}" + view_url = f"{CasedConstants.API_BASE_URL}/deployments/{deployment_id}" deployer_full_name = ( f"{deployment.get('deployer').get('first_name')} {deployment.get('deployer').get('last_name')}" # noqa: E501 if deployment.get("deployer") @@ -74,6 +259,16 @@ def deployments(limit): deployments_data.sort(key=lambda x: x["begin_time"], reverse=True) # Add sorted data to the table + table = Table(title="Recent Deployments") + + table.add_column("Begin Time", style="cyan") + table.add_column("End Time", style="cyan") + table.add_column("Deployer", style="magenta") + table.add_column("Status", style="green") + table.add_column("Branch", style="yellow") + table.add_column("Target", style="blue") + table.add_column("View", style="cyan") + for deployment in deployments_data: table.add_row( deployment["begin_time"].strftime("%Y-%m-%d %H:%M"), @@ -94,9 +289,41 @@ def deployments(limit): console.print(table) +@click.command() +@click.option("--project", default="", help="Project name to filter branches") +@validate_credentials(check_project_set=True) +def targets(project): + """ + Display target environments. + + This command shows a list of target environments for the selected project. + """ + data = run_process_with_status_bar( + CasedAPI().get_targets, "Fetching targets...", timeout=10, project_name=project + ) + targets = data.get("targets", []) + if not targets: + console.print("[red]No targets available.[/red]") + return + + table = Table(title="Targets") + + table.add_column("Name", style="cyan") + + for target in targets: + table.add_row( + target.get("name"), + ) + + console.print(table) + + @click.command() @click.option("--limit", default=5, help="Number of branches to show") -def branches(limit): +@click.option("--project", default="", help="Project name to filter branches") +@click.option("--target", default="", help="Target name to filter branches") +@validate_credentials(check_project_set=True) +def branches(limit, project, target): """ Display active branches. @@ -105,28 +332,44 @@ def branches(limit): Use the --limit option to specify the number of branches to display. """ - token = get_token() - if not token: - console.print("[red]Please log in first using 'cased login'[/red]") - return + if not target: + targets = CasedAPI().get_targets(project).get("targets", []) + if not targets: + console.print( + f"[yellow]No targets available. You can add a target by going to {CasedConstants.BASE_URL+'deployments/'+project+'/targets/new'}[/yellow]" # noqa: E501 + ) + return - table = Table(title="Active Branches") + selection = questionary.select( + "Select a project:", choices=targets, style=custom_style + ).ask() + if not selection: + console.print("[yellow]No target selected.[/yellow]") + return + target = selection - table.add_column("Name", style="cyan") - table.add_column("Author", style="magenta") - table.add_column("PR Number", style="yellow") - table.add_column("PR Title", style="green") - table.add_column("Deployable", style="blue") - table.add_column("Mergeable", style="blue") - table.add_column("Checks", style="cyan") - - data = run_process_with_status_bar(get_branches, "Fetching branches...", timeout=10) + data = run_process_with_status_bar( + CasedAPI().get_branches, + "Fetching branches...", + timeout=10, + project_name=project, + target_name=target, + ) branches = data.get("pull_requests", []) - # Generate fake data for idx, branch in enumerate(branches): if idx == limit: break + table = Table(title="Active Branches") + + table.add_column("Name", style="cyan") + table.add_column("Author", style="magenta") + table.add_column("PR Number", style="yellow") + table.add_column("PR Title", style="green") + table.add_column("Deployable", style="blue") + table.add_column("Mergeable", style="blue") + table.add_column("Checks", style="cyan") + table.add_row( branch.get("branch_name"), branch.get("owner"), diff --git a/cased/utils/api.py b/cased/utils/api.py index 15d3178..b362d09 100644 --- a/cased/utils/api.py +++ b/cased/utils/api.py @@ -1,89 +1,117 @@ -import os +import sys import click import requests from rich.console import Console -console = Console() +from cased.utils.config import load_config +from cased.utils.constants import CasedConstants -API_BASE_URL = os.environ.get( - "CASED_API_BASE_URL", default="https://app.cased.com/api/v1" -) -REQUEST_HEADERS = { - "X-CASED-API-KEY": os.environ.get("CASED_API_AUTH_KEY"), - "X-CASED-ORG-ID": str(os.environ.get("CASED_API_ORG_ID")), - "Accept": "application/json", -} +console = Console() -def get_branches(target_name: str = None): - query_params = {"target_name": target_name} if target_name else {} - response = requests.get( - f"{API_BASE_URL}/prs", - headers=REQUEST_HEADERS, - params=query_params, +# This is a special case, at this moment, users have not logged in yet. +# So leave it out of CasedAPI class. +def validate_tokens(api_token, org_name): + return requests.post( + f"{CasedConstants.API_BASE_URL}/validate-token/", + json={"api_token": api_token, "org_name": org_name}, ) - if response.status_code == 200: - return response.json() - else: - click.echo("Failed to fetch branches. Please try again.") - return [] - -def get_targets(): - response = requests.get(f"{API_BASE_URL}/targets", headers=REQUEST_HEADERS) - if response.status_code == 200: - return response.json() - else: - click.echo("Failed to fetch targets. Please try again.") - return [] +class CasedAPI: + def __init__(self): + configs = load_config(CasedConstants.ENV_FILE) + self.request_headers = { + "X-CASED-API-KEY": str(configs.get(CasedConstants.CASED_API_AUTH_KEY)), + "X-CASED-ORG-ID": str(configs.get(CasedConstants.CASED_ORG_ID)), + "Accept": "application/json", + } -def get_deployments(): - response = requests.get(f"{API_BASE_URL}/deployments/", headers=REQUEST_HEADERS) - if response.status_code == 200: - return response.json() - else: - click.echo("Failed to fetch deployments. Please try again.") - return [] - + def get_branches(self, project_name, target_name): + query_params = {"project_name": project_name, "target_name": target_name} + print(query_params) + response = requests.get( + f"{CasedConstants.API_BASE_URL}/branches", + headers=self.request_headers, + params=query_params, + ) + if response.status_code == 200: + return response.json() + else: + click.echo("Failed to fetch branches. Please try again.") + sys.exit(1) -def deploy_branch(branch_name, target_name): - # Implement branch deployment logic here - response = requests.post( - f"{API_BASE_URL}/branch-deploys/", - json={"branch_name": branch_name, "target_name": target_name}, - headers=REQUEST_HEADERS, - ) - if response.status_code == 200: - click.echo( - f"Successfully deployed branch '{branch_name}' to target '{target_name}'!" + def get_projects(self): + response = requests.get( + f"{CasedConstants.BASE_URL}/deployments", headers=self.request_headers ) - else: - click.echo("Deployment failed. Please check your input and try again.") + if response.status_code == 200: + return response.json() + else: + click.echo("Failed to fetch projects. Please try again.") + sys.exit(1) + def get_targets(self, project_name): + params = {"project_name": project_name} + response = requests.get( + f"{CasedConstants.API_BASE_URL}/targets", + headers=self.request_headers, + params=params, + ) + if response.status_code == 200: + return response.json() + else: + click.echo("Failed to fetch targets. Please try again.") + sys.exit(1) -def create_secrets(project_name: str, secrets: list): - payload = { - "storage_destination": "github_repository", - "keys": [{"name": secret, "type": "credentials"} for secret in secrets], - } - response = requests.post( - f"{API_BASE_URL}/api/v1/secrets/{project_name}/setup", - json=payload, - headers=REQUEST_HEADERS, - ) - if response.status_code == 201: - console.print("[green]Secrets setup successful![/green]") - console.print( - f"Please go to {API_BASE_URL}/secrets/{project_name} to update these secrets." # noqa: E501 + def get_deployments(self, project_name, target_name): + response = requests.get( + f"{CasedConstants.API_BASE_URL}/targets/{project_name}/{target_name}/deployments/", + headers=self.request_headers, ) - else: - console.print( - f"[yellow]Secrets setup returned status code {response.status_code}.[/yellow]" # noqa: E501 + if response.status_code == 200: + print(response.json()) + return response.json() + else: + click.echo("Failed to fetch deployments. Please try again.") + sys.exit(1) + + def deploy_branch(self, branch_name, target_name): + # Implement branch deployment logic here + response = requests.post( + f"{CasedConstants.API_BASE_URL}/branch-deploys/", + json={"branch_name": branch_name, "target_name": target_name}, + headers=self.request_headers, ) - console.print( - "Please go to your GitHub repository settings to manually set up the following secrets:" # noqa: E501 + if response.status_code == 200: + click.echo( + f"Successfully deployed branch '{branch_name}' to target '{target_name}'!" + ) + else: + sys.exit(1) + + def create_secrets(self, project_name: str, secrets: list): + payload = { + "storage_destination": "github_repository", + "keys": [{"name": secret, "type": "credentials"} for secret in secrets], + } + response = requests.post( + f"{CasedConstants.API_BASE_URL}/api/v1/secrets/{project_name}/setup", + json=payload, + headers=self.request_headers, ) - for secret in secrets: - console.print(f"- {secret}") + if response.status_code == 201: + console.print("[green]Secrets setup successful![/green]") + console.print( + f"Please go to {CasedConstants.API_BASE_URL}/secrets/{project_name} to update these secrets." # noqa: E501 + ) + else: + console.print( + f"[yellow]Secrets setup returned status code {response.status_code}.[/yellow]" # noqa: E501 + ) + console.print( + "Please go to your GitHub repository settings to manually set up the following secrets:" # noqa: E501 + ) + for secret in secrets: + console.print(f"- {secret}") diff --git a/cased/utils/auth.py b/cased/utils/auth.py index f5a9584..24ff924 100644 --- a/cased/utils/auth.py +++ b/cased/utils/auth.py @@ -1,23 +1,60 @@ -import json -import os -from datetime import datetime +from functools import wraps -# Configuration -CONFIG_DIR = os.path.expanduser("~/.cased/config") -TOKEN_FILE = os.path.join(CONFIG_DIR, "session_token") +from rich.console import Console +from rich.panel import Panel +from cased.utils.config import load_config +from cased.utils.constants import CasedConstants -def get_token(): - if os.path.exists(TOKEN_FILE): - with open(TOKEN_FILE, "r") as f: - data = json.load(f) - temp_file = data.get("temp_file") +console = Console() - if temp_file and os.path.exists(temp_file): - with open(temp_file, "r") as tf: - token_data = json.load(tf) - expiry = datetime.fromisoformat(token_data["expiry"]) - if datetime.now() < expiry: - return token_data["token"] - return None +def validate_credentials(check_project_set=False): + def decorator(func): + def _validate_config(config): + return config and all( + [ + config.get(CasedConstants.CASED_API_AUTH_KEY), + config.get(CasedConstants.CASED_ORG_ID), + ] + ) + + def _validate_project_set(config): + return config and all( + [ + config.get(CasedConstants.CASED_WORKING_PROJECT_NAME), + config.get(CasedConstants.CASED_WORKING_PROJECT_ID), + ] + ) + + @wraps(func) + def wrapper(*args, **kwargs): + config = load_config(CasedConstants.ENV_FILE) + if not _validate_config(config): + console.print( + Panel( + "[bold red]You are not logged in.[/bold red]\nPlease run 'cased login' first.", # noqa: E501 + title="Authentication Error", + expand=False, + ) + ) + return + elif check_project_set: + if not _validate_project_set(config): + console.print( + Panel( + "[bold red]You have not selected a project yet.[/bold red]\nPlease run 'cased projects' first or provide a project name with the --project flag.", # noqa: E501 + title="Project Error", + expand=False, + ) + ) + return + kwargs["project"] = config.get( + CasedConstants.CASED_WORKING_PROJECT_NAME + ) + + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/cased/utils/config.py b/cased/utils/config.py new file mode 100644 index 0000000..a9da448 --- /dev/null +++ b/cased/utils/config.py @@ -0,0 +1,32 @@ +import os + +from cased.utils.constants import CasedConstants + + +def load_config(file_path=CasedConstants.ENV_FILE): + if not os.path.exists(file_path): + return None + config = {} + with open(file_path, "r") as f: + for line in f: + key, value = line.strip().split("=", 1) + config[key] = value + return config + + +def save_config( + data, config_dir=CasedConstants.CONFIG_DIR, file_name=CasedConstants.ENV_FILE +): + os.makedirs(config_dir, mode=0o700, exist_ok=True) + current_config = load_config(file_name) + if not current_config: + current_config = {} + current_config.update(data) + with open(file_name, "w") as f: + for key, value in current_config.items(): + f.write(f"{key}={value}\n") + + +def delete_config(file_name=CasedConstants.ENV_FILE): + if os.path.exists(file_name): + os.remove(file_name) diff --git a/cased/utils/constants.py b/cased/utils/constants.py new file mode 100644 index 0000000..7c0f987 --- /dev/null +++ b/cased/utils/constants.py @@ -0,0 +1,22 @@ +"""Constants used in the CLI""" + +import os + + +class CasedConstants: + + ### Config files + CONFIG_DIR = os.path.expanduser("~/.cased/config") + ENV_FILE = os.path.join(CONFIG_DIR, "env") + + ### API Constants + CASED_API_AUTH_KEY = "CASED_API_AUTH_KEY" + CASED_ORG_ID = "CASED_ORG_ID" + CASED_ORG_NAME = "CASED_ORG_NAME" + + BASE_URL = os.environ.get("CASED_BASE_URL", default="https://app.cased.com/") + API_BASE_URL = BASE_URL + "/api/v1" + + # Project related constants + CASED_WORKING_PROJECT_NAME = "CASED_WORKING_PROJECT_NAME" + CASED_WORKING_PROJECT_ID = "CASED_WORKING_PROJECT_ID" diff --git a/cased/utils/progress.py b/cased/utils/progress.py index 5b27ebb..43aa6b9 100644 --- a/cased/utils/progress.py +++ b/cased/utils/progress.py @@ -16,15 +16,8 @@ def run_process_with_status_bar( console = Console() result = None - def update_progress(progress, task): - start_time = time.time() - while time.time() - start_time < timeout: - elapsed = int(time.time() - start_time) - progress.update(task, completed=min(elapsed, timeout)) - time.sleep(1) # Update every second - with Progress() as progress: - task = progress.add_task(f"[green]{description}", total=timeout) + task = progress.add_task(f"[green]{description}", total=100) with ThreadPoolExecutor(max_workers=2) as executor: # Submit the main process @@ -32,14 +25,13 @@ def update_progress(progress, task): start_time = time.time() while not future.done() and time.time() - start_time < timeout: elapsed = int(time.time() - start_time) - progress.update(task, completed=min(elapsed, timeout)) + steps = (elapsed / timeout) * 100 + progress.update(task, completed=min(steps, 100)) time.sleep(0.1) # Update frequently for responsiveness try: result = future.result(timeout=timeout) - progress.update( - task, completed=timeout, description="[bold green]Done!" - ) + progress.update(task, completed=100, description="[bold green]Done!") except TimeoutError: progress.update(task, description="[bold red]Timeout!") console.print( diff --git a/poetry.lock b/poetry.lock index 7fb981f..8b54266 100644 --- a/poetry.lock +++ b/poetry.lock @@ -29,13 +29,13 @@ wcwidth = ">=0.1.4" [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] @@ -173,6 +173,20 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +optional = false +python-versions = "*" +files = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + [[package]] name = "distlib" version = "0.3.8" @@ -201,29 +215,29 @@ xmod = "*" [[package]] name = "filelock" -version = "3.15.4" +version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "identify" -version = "2.6.0" +version = "2.6.1" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, - {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, + {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, + {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, ] [package.extras] @@ -231,15 +245,18 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "inquirer" version = "3.4.0" @@ -301,30 +318,6 @@ files = [ [package.dependencies] ansicon = {version = "*", markers = "platform_system == \"Windows\""} -[[package]] -name = "markdown-it-py" -version = "2.2.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.7" -files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "markupsafe" version = "2.1.5" @@ -394,17 +387,6 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - [[package]] name = "nodeenv" version = "1.9.1" @@ -418,19 +400,19 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pre-commit" @@ -452,13 +434,13 @@ virtualenv = ">=20.10.0" [[package]] name = "prompt-toolkit" -version = "3.0.47" +version = "3.0.36" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.6.2" files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, ] [package.dependencies] @@ -466,17 +448,16 @@ wcwidth = "*" [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] @@ -557,20 +538,17 @@ files = [ [[package]] name = "questionary" -version = "1.10.0" +version = "2.0.1" description = "Python library to build pretty command line user prompts ⭐️" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.8" files = [ - {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, - {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, + {file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"}, + {file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"}, ] [package.dependencies] -prompt_toolkit = ">=2.0,<4.0" - -[package.extras] -docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)"] +prompt_toolkit = ">=2.0,<=3.0.36" [[package]] name = "readchar" @@ -585,13 +563,13 @@ files = [ [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -606,47 +584,47 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.7.1" +version = "12.6.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.6.3,<4.0.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, + {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, ] [package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" [package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] [[package]] name = "ruff" -version = "0.6.2" +version = "0.6.8" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, - {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, - {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, - {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, - {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, - {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, - {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, + {file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"}, + {file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"}, + {file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"}, + {file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"}, + {file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"}, + {file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"}, + {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"}, ] [[package]] @@ -676,30 +654,30 @@ files = [ [[package]] name = "urllib3" -version = "2.0.7" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.3" +version = "20.26.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, + {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, + {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, ] [package.dependencies] @@ -735,5 +713,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "c2f24afce3179803aa334bbc58e86bdd374a3490ce83bc1eccaa4b2f13a6756c" +python-versions = "^3.12" +content-hash = "619c31e510dbccccf0ad264bdac0a7e7af5c545009afeba4c167e2699ded5470" diff --git a/pyproject.toml b/pyproject.toml index 2a3e987..3e0e8bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ repository = "https://github.com/cased/cli" packages = [{include = "cased"}] [tool.poetry.dependencies] -python = "^3.9" +python = "^3.12" click = "*" requests = "*" rich = "*" From 537f6df426b41b872c79ab58eff20c706d7d187d Mon Sep 17 00:00:00 2001 From: Rick Chen Date: Tue, 1 Oct 2024 22:30:05 -0700 Subject: [PATCH 2/7] fix --- cased/utils/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cased/utils/api.py b/cased/utils/api.py index b362d09..0b45619 100644 --- a/cased/utils/api.py +++ b/cased/utils/api.py @@ -30,7 +30,6 @@ def __init__(self): def get_branches(self, project_name, target_name): query_params = {"project_name": project_name, "target_name": target_name} - print(query_params) response = requests.get( f"{CasedConstants.API_BASE_URL}/branches", headers=self.request_headers, @@ -71,7 +70,6 @@ def get_deployments(self, project_name, target_name): headers=self.request_headers, ) if response.status_code == 200: - print(response.json()) return response.json() else: click.echo("Failed to fetch deployments. Please try again.") From 69a9bd3335df2f797c9c37eba3146145411cdc3b Mon Sep 17 00:00:00 2001 From: Rick Chen Date: Tue, 1 Oct 2024 22:36:18 -0700 Subject: [PATCH 3/7] comment --- cased/commands/resources.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cased/commands/resources.py b/cased/commands/resources.py index c871153..9003c2a 100644 --- a/cased/commands/resources.py +++ b/cased/commands/resources.py @@ -111,7 +111,6 @@ def projects(details=True): ] if details: - # Create a table to display projects table = Table(title="Projects Details", box=box.ROUNDED) table.add_column("ID", style="cyan", no_wrap=True) table.add_column("Repository", style="magenta") @@ -139,7 +138,6 @@ def projects(details=True): console.print( "[yellow]No project selected. Please select a project from the list below:[/yellow]" ) - # Prepare choices for questionary choices = ["Exit without changing project"] choices.extend( [ @@ -148,7 +146,6 @@ def projects(details=True): ] ) - # Prompt user for selection using questionary try: selection = questionary.select( "Select a project:", choices=choices, style=custom_style From 9f869ed7829c38d48f2c9b9df4532eb9960c949c Mon Sep 17 00:00:00 2001 From: Rick Chen Date: Wed, 2 Oct 2024 10:24:52 -0700 Subject: [PATCH 4/7] fix and doc --- cased/commands/login.py | 32 ++++++++++++++++++++++++++++++++ cased/commands/resources.py | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/cased/commands/login.py b/cased/commands/login.py index efa6a39..8c1b951 100644 --- a/cased/commands/login.py +++ b/cased/commands/login.py @@ -1,3 +1,35 @@ +""" +Cased CLI Authentication Module + +This module provides command-line interface (CLI) functionality for authenticating +with the Cased system. It includes commands for logging in and out of the Cased +CLI, handling API key validation, and managing user sessions. + +Dependencies: + - click: For creating CLI commands + - rich: For enhanced console output and user interaction + - cased: Custom package for Cased-specific functionality + +Commands: + - login: Initiates the login process, validates credentials, and stores session information + - logout: Removes locally stored credentials and logs the user out of the Cased CLI + +The module uses the Rich library to provide a visually appealing and interactive +console interface, including progress bars and styled text output. + +Usage: + To use this module, import it into your main CLI application and add the + login and logout commands to your command group. + +Note: + This module assumes the existence of various utility functions and constants + from the cased package, which should be properly set up for the module to function correctly. + +Author: Cased +Date: 10/01/2024 +Version: 1.0 +""" + import click from rich.console import Console from rich.panel import Panel diff --git a/cased/commands/resources.py b/cased/commands/resources.py index 9003c2a..3e799ab 100644 --- a/cased/commands/resources.py +++ b/cased/commands/resources.py @@ -81,7 +81,7 @@ default=True, help="Show detailed information about projects", ) -@validate_credentials +@validate_credentials(check_project_set=False) def projects(details=True): """ Display and select Cased projects. From c46c616d2be79e2f2041cb13dbc4132c0c15f576 Mon Sep 17 00:00:00 2001 From: Rick Chen Date: Wed, 2 Oct 2024 10:25:47 -0700 Subject: [PATCH 5/7] remove --- cased/cli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cased/cli.py b/cased/cli.py index fa94558..f737aa0 100644 --- a/cased/cli.py +++ b/cased/cli.py @@ -30,8 +30,6 @@ def cli(): cli.add_command(projects) cli.add_command(targets) -# ... (keep the login and setup_target commands as they were) ... - if __name__ == "__main__": cli() From fe618b4e7b19f811d7caa02b9ebecde3560713f5 Mon Sep 17 00:00:00 2001 From: Rick Chen Date: Wed, 2 Oct 2024 10:53:38 -0700 Subject: [PATCH 6/7] slash --- cased/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cased/utils/constants.py b/cased/utils/constants.py index 7c0f987..6d9ca69 100644 --- a/cased/utils/constants.py +++ b/cased/utils/constants.py @@ -14,7 +14,7 @@ class CasedConstants: CASED_ORG_ID = "CASED_ORG_ID" CASED_ORG_NAME = "CASED_ORG_NAME" - BASE_URL = os.environ.get("CASED_BASE_URL", default="https://app.cased.com/") + BASE_URL = os.environ.get("CASED_BASE_URL", default="https://app.cased.com") API_BASE_URL = BASE_URL + "/api/v1" # Project related constants From 73471bf68dae41711bd7135552c38b47fb06e4de Mon Sep 17 00:00:00 2001 From: Rick Chen Date: Wed, 2 Oct 2024 12:18:41 -0700 Subject: [PATCH 7/7] version --- cased/commands/login.py | 4 ++-- cased/commands/resources.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cased/commands/login.py b/cased/commands/login.py index 8c1b951..8650a9c 100644 --- a/cased/commands/login.py +++ b/cased/commands/login.py @@ -27,7 +27,7 @@ Author: Cased Date: 10/01/2024 -Version: 1.0 +Version: 1.0.0 """ import click @@ -116,7 +116,7 @@ def login(): @click.command() -@validate_credentials +@validate_credentials(check_project_set=False) def logout(): """ Log out from your Cased account. diff --git a/cased/commands/resources.py b/cased/commands/resources.py index 3e799ab..a1f4e8f 100644 --- a/cased/commands/resources.py +++ b/cased/commands/resources.py @@ -29,7 +29,7 @@ Author: Cased Date: 10/01/2024 -Version: 1.0 +Version: 1.0.0 """ # noqa: E501 import click