diff --git a/.ci-pre-commit-config.yaml b/.ci-pre-commit-config.yaml new file mode 100644 index 0000000..63cdd73 --- /dev/null +++ b/.ci-pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.291 + hooks: + - id: ruff + args: [--line-length=100] + + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..e4f4860 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,16 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: blacksmith-4vcpu-ubuntu-2204 + steps: + - uses: actions/checkout@v3 + - uses: useblacksmith/setup-python@v6 + - uses: pre-commit/action@v3.0.0 + with: + extra_args: --all-files --config .ci-pre-commit-config.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c34b3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# .gitignore +_pycache_/ +__pycache__/ +.egg-info/ +env/ +.env/ +venv/ +.DS_STORE +.pre-commit-config.yaml +.pem diff --git a/README.md b/README.md new file mode 100644 index 0000000..5587cb1 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# Cased CLI + +## Overview + +Cased CLI is a powerful command-line interface tool designed to streamline deployment processes and manage branches efficiently. It provides an intuitive way to interact with the Cased system, offering functionalities such as user authentication, deployment management, and branch oversight. + +## Features + +- User authentication (login/logout) +- View recent deployments +- Display active branches +- Interactive deployment process with branch and target selection +- Comprehensive help system + +## Installation + +You can install the Cased CLI tool using pip: + +``` +pip install cased +``` + +This will install the latest version of the tool from PyPI. + +## Usage + +After installation, you can use the CLI tool by running `cased` followed by a command: + +``` +cased --help +``` + +### Available Commands: + +- `cased login`: Log in to the Cased system +- `cased logout`: Log out from the Cased system +- `cased deployments`: View recent deployments +- `cased branches`: View active branches +- `cased deploy`: Deploy a branch to a target environment + +For more details on each command, use: + +``` +cased COMMAND --help +``` + +## Development + +To set up the Cased CLI for development: + +1. Clone the repository: + ``` + git clone https://github.com/cased/cli.git + cd cli + ``` + +2. Install Poetry (if not already installed): + ``` + pip install poetry + ``` + +3. Install dependencies: + ``` + poetry install + ``` + +4. Activate the virtual environment: + ``` + poetry shell + ``` + +5. Run the CLI in development mode: + ``` + poetry run cased + ``` + +### Making Changes + +1. Make your changes to the codebase. +2. Update tests if necessary. +3. Run tests: + ``` + poetry run pytest + ``` +4. Build the package: + ``` + poetry build + ``` + +### Submitting Changes + +1. Create a new branch for your changes: + ``` + git checkout -b feature/your-feature-name + ``` +2. Commit your changes: + ``` + git commit -am "Add your commit message" + ``` +3. Push to your branch: + ``` + git push origin feature/your-feature-name + ``` +4. Create a pull request on GitHub. + +## Contact + +For any questions or support, please contact cli@cased.com diff --git a/cased/.vscode/settings.json b/cased/.vscode/settings.json new file mode 100644 index 0000000..f67a951 --- /dev/null +++ b/cased/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.trimAutoWhitespace": true, + "editor.formatOnSave": true, + "editor.formatOnType": true + }, + "files.trimTrailingWhitespace": true, + "[html][django-html][handlebars][hbs][mustache][jinja][jinja-html][nj][njk][nunjucks][twig]": { + "editor.defaultFormatter": "monosans.djlint" + } +} diff --git a/cased/__init__.py b/cased/__init__.py new file mode 100644 index 0000000..3dc1f76 --- /dev/null +++ b/cased/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/cased/cli.py b/cased/cli.py new file mode 100644 index 0000000..62198d7 --- /dev/null +++ b/cased/cli.py @@ -0,0 +1,38 @@ +import click + +from cased.commands.build import build +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, projects, targets + +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + + +@click.group() +def cli(): + """ + Cased CLI for authentication, target setup, and branch deployment. + + Use 'cased COMMAND --help' for more information on a specific command. + """ + + pass + + +cli.add_command(deploy) +cli.add_command(init) +cli.add_command(build) +cli.add_command(login) +cli.add_command(logout) +cli.add_command(deployments) +cli.add_command(branches) +cli.add_command(projects) +cli.add_command(targets) + + +if __name__ == "__main__": + try: + cli() + except Exception as _: + click.echo("\nProcess interrupted. Exiting.") diff --git a/cased/commands/__init__.py b/cased/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cased/commands/build.py b/cased/commands/build.py new file mode 100644 index 0000000..ca4e94f --- /dev/null +++ b/cased/commands/build.py @@ -0,0 +1,177 @@ +import os +import sys +from pathlib import Path +from typing import Any, Dict, List + +import click +import yaml +from jinja2 import Environment, FileSystemLoader +from rich.console import Console +from rich.panel import Panel +from rich.progress import Progress, SpinnerColumn, TextColumn + +from cased.utils.api import CasedAPI +from cased.utils.constants import CasedConstants +from cased.utils.git import get_repo_name + +console = Console() +# Get the directory of the current script +CURRENT_DIR = Path(__file__).resolve().parent +# Go up one level to the 'cased' directory +CASED_DIR = CURRENT_DIR.parent +# Set the path to the templates directory +TEMPLATES_DIR = CASED_DIR / "templates" +CONFIG_PATH = ".cased/config.yaml" + + +@click.command() +def build() -> None: + """ + Generate a GitHub Actions workflow based on the configuration in .cased/config.yaml. + + This command reads the configuration file, validates it, generates a workflow file + in the .github/workflows directory, and sets up necessary secrets. + """ + if not os.path.exists(CONFIG_PATH): + console.print( + "[red]Error: Configuration file not found at .cased/config.yaml[/red]" + ) + console.print( + "Please run 'cased init' to generate the configuration file first." + ) + sys.exit(1) + + config = load_config(CONFIG_PATH) + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + ) as progress: + validate_task = progress.add_task( + "[cyan]Validating configuration...", total=100 + ) + try: + validate_config(config) + progress.update( + validate_task, + completed=100, + description="[bold green]Configuration validated successfully!", + ) + except ValueError as e: + progress.update(validate_task, completed=100) + console.print(f"[red]Configuration validation failed: {str(e)}[/red]") + sys.exit(1) + + generate_task = progress.add_task("[cyan]Generating workflow...", total=100) + workflow_content = generate_workflow(config) + save_workflow(workflow_content) + progress.update( + generate_task, + completed=100, + description="[bold green]Workflow generated successfully!", + ) + + project_name = get_repo_name() + secrets = extract_secrets_from_workflow(workflow_content) + CasedAPI().create_secrets(project_name, secrets) + + console.print( + Panel( + f""" + [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 {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 {CasedConstants.API_BASE_URL}/deployments/ to monitor the deployment status. + """, # noqa: E501 + title="Success", + expand=False, + ) + ) + + +def load_config(file_path: str) -> Dict[str, Any]: + with open(file_path, "r") as file: + return yaml.safe_load(file) + + +def generate_workflow(config: Dict[str, Any]) -> str: + env = Environment(loader=FileSystemLoader(TEMPLATES_DIR)) + + if config["docker"]["enabled"]: + template = env.get_template("docker_ec2_template.yaml") + else: + template = env.get_template("non_docker_ec2_template.yaml") + + return template.render(config=config) + + +def save_workflow(content: str) -> None: + os.makedirs(".github/workflows", exist_ok=True) + with open(".github/workflows/deploy.yaml", "w") as file: + file.write(content) + + +def extract_secrets_from_workflow(workflow_content: str) -> List[str]: + secrets = [] + for line in workflow_content.split("\n"): + if "secrets." in line: + secret = line.split("secrets.")[1].split("}")[0].strip() + if secret not in secrets: + secrets.append(secret) + return secrets + + +def validate_config(config: Dict[str, Any]) -> None: + # Project validation + if "project" not in config: + raise ValueError("Missing 'project' section in config") + if "name" not in config["project"]: + raise ValueError("Missing 'name' in 'project' section") + + # Environment validation + if "environment" not in config: + raise ValueError("Missing 'environment' section in config") + required_env_fields = ["language", "python_version"] + for field in required_env_fields: + if field not in config["environment"]: + raise ValueError(f"Missing '{field}' in 'environment' section") + + # Docker validation + if "docker" not in config: + raise ValueError("Missing 'docker' section in config") + if "enabled" not in config["docker"]: + raise ValueError("Missing 'enabled' field in 'docker' section") + + if config["docker"]["enabled"]: + required_docker_fields = [ + "ECR Repository Name", + "dockerfile_path", + "image_name", + ] + for field in required_docker_fields: + if field not in config["docker"]: + raise ValueError(f"Missing '{field}' in 'docker' section") + + if not isinstance(config["docker"].get("ports", []), list): + raise ValueError("'ports' in 'docker' section must be a list") + + if "environment" in config["docker"] and not isinstance( + config["docker"]["environment"], list + ): + raise ValueError("'environment' in 'docker' section must be a list") + else: + # Non-docker validation + if "runtime" not in config: + raise ValueError("Missing 'runtime' section in config for non-docker setup") + required_runtime_fields = ["commands", "entry_point"] + for field in required_runtime_fields: + if field not in config["runtime"]: + raise ValueError(f"Missing '{field}' in 'runtime' section") + + required_commands = ["start", "stop", "restart"] + for command in required_commands: + if command not in config["runtime"]["commands"]: + raise ValueError(f"Missing '{command}' in 'runtime.commands' section") diff --git a/cased/commands/deploy.py b/cased/commands/deploy.py new file mode 100644 index 0000000..5996246 --- /dev/null +++ b/cased/commands/deploy.py @@ -0,0 +1,96 @@ +import sys + +import click +import questionary +from rich.console import Console + +from cased.utils.api import CasedAPI +from cased.utils.auth import validate_credentials +from cased.utils.constants import CasedConstants +from cased.utils.progress import run_process_with_status_bar + +console = Console() + + +def _build_questionary_choices(project): + api_client = CasedAPI() + data = run_process_with_status_bar( + api_client.get_branches, + f"Fetching branches for project {project}...", + timeout=10, + project_name=project, + ) + branches = data.get("pull_requests", []) + deployable_branches = [ + branch for branch in branches if branch["deployable"] is True + ] + # Prepare choices for questionary + choices = [ + f"{b['branch_name']} -> [{', '.join([target.get('name') for target in b.get('targets', [])])}]" # noqa: E501 + for b in deployable_branches + ] + + if not choices: + console.print( + f"[red]No deployable branches for project {project}. Please see more details at {CasedConstants.BASE_URL}/projects/{project} [/red]" # noqa: E501 + ) + sys.exit(1) + + selected = questionary.select("Select a branch to deploy:", choices=choices).ask() + + if not selected: + console.print("[red]Error: No branch selected.[/red]") + sys.exit(1) + + branch = selected.split(" -> ")[0] + + # Find the selected branch in our data + selected_branch = next( + (b for b in deployable_branches if b["branch_name"] == branch), None + ) + if not selected_branch: + console.print(f"[red]Error: Branch {branch} is not deployable.[/red]") + sys.exit(1) + + available_targets = selected_branch["targets"] + if not available_targets: + console.print(f"[red]Error: No targets available for branch {branch}.[/red]") + sys.exit(1) + target = questionary.select( + "Select a target environment:", choices=available_targets + ).ask() + + return branch, target + + +@click.command() +@click.option("--project", help="Project name to deploy") +@click.option("--branch", help="Branch to deploy") +@click.option("--target", help="Target environment for deployment") +@validate_credentials(check_project_set=True) +def deploy(project, branch, target): + """ + Deploy a branch to a target environment. + + This command allows you to deploy a specific branch to a target environment. + If --branch or --target are not provided, you will be prompted to select from available options. + + The command will display deployable branches with their available targets, initiate the deployment process, + and show the deployment progress. + + Examples: + cased deploy + cased deploy --branch feature-branch-1 --target dev + """ # noqa: E501 + if not branch and not target: + branch, target = _build_questionary_choices(project) + + console.print( + f"Preparing to deploy [cyan]{branch}[/cyan] to [yellow]{target}[/yellow]" + ) + + if branch and target: + CasedAPI().deploy_branch(project, 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 new file mode 100644 index 0000000..6735fab --- /dev/null +++ b/cased/commands/init.py @@ -0,0 +1,223 @@ +import os + +import click +import inquirer +import yaml +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 (alpha)""" + console.print(Panel.fit("Welcome to Cased", style="bold magenta")) + + config = {} + config.update(get_project_info()) + config.update(get_environment_info()) + config.update(get_deployment_info()) + + run_process_with_status_bar( + generate_config_file, description="Generating config file...", config=config + ) + + display_results() + + +def get_project_info(): + questions = [inquirer.Text("name", message="Enter your project name")] + answers = inquirer.prompt(questions) + + return {"project": {"name": answers["name"]}} + + +def get_environment_info(): + questions = [ + inquirer.List( + "language", + message="Select the primary language for your project", + choices=["Python", "JavaScript"], + ), + inquirer.List( + "framework", + message="Select the framework (optional)", + choices=["None", "Django", "Flask", "Node.js"], + ), + ] + + answers = inquirer.prompt(questions) + + environment = { + "environment": { + "language": answers["language"], + "framework": answers["framework"], + }, + } + if answers["language"] == "Python": + environment["environment"]["dependency_manager"] = "poetry" + environment["environment"]["python_version"] = "[REQUIRED] " + + return environment + + +def get_deployment_info(): + questions = [ + inquirer.List( + "deployment_target", + message="Select your deployment target", + choices=["AWS", "Custom"], + ), + ] + + answers = inquirer.prompt(questions) + + deployment_info = { + "cloud_deployment": { + "provider": answers["deployment_target"], + }, + } + + return deployment_info + + +def check_for_docker(config): + def find_dockerfile(start_path="."): + for root, dirs, files in os.walk(start_path): + if "Dockerfile" in files: + return os.path.join(root, "Dockerfile") + return None + + dockerfile_path = find_dockerfile() + if dockerfile_path: + config["docker"] = { + "enabled": True, + "ECR Repository Name": "[REQUIRED] ", + "dockerfile_path": dockerfile_path, + "build_args": [ + "[OPTIONAL]", + "=", + "=", + ], + "image_name": "[OPTIONAL] ", + "environment": [ + "[OPTIONAL]", + "=", + "=", + ], + "ports": ["[OPTIONAL] :"], + } + else: + config["docker"] = {"enabled": False} + + +def expand_config_with_placeholders(config): + config["project"]["version"] = "<[OPTIONAL] PROJECT_VERSION>" + config["project"]["description"] = "<[OPTIONAL] PROJECT_DESCRIPTION>" + + config["environment"]["dependency_files"] = [ + "<[OPTIONAL] Cased build will smart detect these files if not provided here>" + ] + if config["docker"]["enabled"]: + config["runtime"] = { + "entry_point": "docker", + } + else: + config["runtime"] = { + "entry_point": "[REQUIRED]", # noqa: E501 + "flags": ["[OPTIONAL]", "", ""], + "commands": { + "start": "", + "stop": "", + "restart": "", + }, + } + + config["cloud_deployment"] = config.get("cloud_deployment", {}) + config["cloud_deployment"].update( + { + "region": "", + "instance_type": "", + "autoscaling": { + "enabled": True, + "min_instances": "", + "max_instances": "", + }, + "load_balancer": {"enabled": True, "type": ""}, + } + ) + + return config + + +def rearrange_config_sections(config): + return { + "project": config["project"], + "environment": config["environment"], + "runtime": config["runtime"], + "docker": config["docker"], + "cloud_deployment": config["cloud_deployment"], + } + + +def write_config_file(config): + comments = """# CASED Configuration File +# +# This file contains the configuration for your project's DevOps processes. +# Please read the following instructions carefully before editing this file. +# +# Instructions: +# 1. Required fields are marked with [REQUIRED]. These must be filled out for the tool to function properly. +# 2. Optional fields are marked with [OPTIONAL]. Fill these out if they apply to your project. +# 3. Fields with default values are pre-filled. Modify them as needed. +# 4. Do not change the structure of this file (i.e., don't remove or rename sections). +# 5. Use quotes around string values, especially if they contain special characters. +# 6. For boolean values, use true or false (lowercase, no quotes). +# 7. For lists, maintain the dash (-) format for each item. +# +# Sections: +# - Project Metadata: Basic information about your project. All fields are required. +# - Environment Configuration: Specify your project's runtime environment. Python or Node version is required if applicable. +# - Application Runtime Configuration: Define how your application runs. The entry_point is required. +# - Docker Configuration: Required if you're using Docker. Set enabled to false if not using Docker. +# - Cloud Deployment Configuration: Required if deploying to a cloud provider. +# +# After editing this file, run 'cased build' to generate your GitHub Actions workflow. +# If you need help, refer to the documentation or run 'cased --help'. + + """ # noqa: E501 + os.makedirs(".cased", exist_ok=True) + with open(".cased/config.yaml", "w") as f: + f.write(f"{comments}\n") + for section, content in config.items(): + yaml.dump({section: content}, f, default_flow_style=False) + f.write("\n") # Add a blank line between sections + + +def generate_config_file(config): + check_for_docker(config) + config = expand_config_with_placeholders(config) + config = rearrange_config_sections(config) + + write_config_file(config) + + +def display_results(): + console.print( + Panel.fit("Configuration files created successfully!", style="bold green") + ) + console.print("Configuration file: [bold].cased/config.yaml[/bold]") + + console.print("\n[bold yellow]Next steps:[/bold yellow]") + console.print("1. Review and edit the configuration files in the .cased directory.") + console.print( + "2. Replace all placeholder values (enclosed in < >) with your actual configuration." # noqa: E501 + ) + console.print( + "3. Once you've updated the config, run [bold]'cased build'[/bold] to generate your GitHub Actions workflow." # noqa: E501 + ) diff --git a/cased/commands/login.py b/cased/commands/login.py new file mode 100644 index 0000000..075225c --- /dev/null +++ b/cased/commands/login.py @@ -0,0 +1,128 @@ +""" +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.0 +""" + +import click +from rich.console import Console +from rich.panel import Panel +from rich.progress import Progress + +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() + + +@click.command() +def login(): + """ + Log in to the Cased system. + + This command initiates a login process, stores a session token, + and provides information about the session expiration. + """ + console.print(Panel("Welcome to Cased CLI", style="bold blue")) + + org_name = click.prompt("Enter your organization name") + api_key = click.prompt("Enter your API key") + + with Progress() as progress: + task = progress.add_task("[cyan]Validating credentials...", total=100) + + progress.update(task, advance=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. + 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, + ) + ) + return + elif response.status_code == 404: + console.print( + Panel( + 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( + f"[bold red]Login failed:[/bold red] {data.get('reason', 'Unknown error')}", + title="Error", + expand=False, + ) + ) + + +@click.command() +@validate_credentials(check_project_set=False) +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 new file mode 100644 index 0000000..fbb2720 --- /dev/null +++ b/cased/commands/resources.py @@ -0,0 +1,357 @@ +""" +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.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 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(check_project_set=False) +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. + + Use the --details or --d option to show detailed information about each project, by default it is set to True. + """ # 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: + 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]" + ) + choices = ["Exit without changing project"] + choices.extend( + [ + f"{project['id']} - {project['repository_full_name']} ({project['code_host']})" + for project in projects + ] + ) + + 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") +@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. + + This command shows a table of recent deployments, including information + such as begin time, end time, deployer, status, branch, and target. + + Use the --limit option to specify the number of deployments to display. + Use the --project and --target options to filter deployments by project and target. + """ + 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): + if idx == limit: + break + begin_time = parser.parse(deployment.get("start_time")) + end_time = ( + parser.parse(deployment.get("end_time")) + if deployment.get("end_time") + else "" + ) + status = deployment.get("status", "Unknown") + deployment_id = deployment.get("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") + else "Unknown" + ) + + deployments_data.append( + { + "begin_time": begin_time, + "end_time": end_time, + "deployer": deployer_full_name, + "status": status, + "branch": deployment.get("ref").replace("refs/heads/", ""), + "target": deployment.get("target").get("name"), + "view": (deployment_id, view_url), + } + ) + + # Sort deployments by start time in descending order + 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"), + ( + deployment["end_time"].strftime("%Y-%m-%d %H:%M") + if deployment["end_time"] + else "NULL" + ), + deployment["deployer"], + deployment["status"], + deployment["branch"], + deployment["target"], + Text( + f"View {deployment['view'][0]}", style=f"link {deployment['view'][1]}" + ), + ) + + 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") +@click.option("--project", default="", help="Project name to filter branches") +@validate_credentials(check_project_set=True) +def branches(limit, project): + """ + Display active branches. + + This command shows a table of active branches, including information + such as name, author, PR number, PR title, deployable status, and various checks. + + Use the --limit option to specify the number of branches to display. + Use the --project option to filter branches by project. + """ + data = run_process_with_status_bar( + CasedAPI().get_branches, + "Fetching branches...", + timeout=10, + project_name=project, + ) + branches = data.get("pull_requests", []) + print(len(branches)) + + 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") + for idx, branch in enumerate(branches): + if idx == limit: + break + + table.add_row( + branch.get("branch_name"), + branch.get("owner"), + str(branch.get("number")), + branch.get("title"), + str(branch.get("deployable")), + str(branch.get("mergeable")), + ", ".join( + [ + f"approved: {branch.get('approved')}", + f"up-to-date: {branch.get('up_to_date')}", + f"checks-passed: {branch.get('checks_passing')}", + ] + ), + ) + + console.print(table) diff --git a/cased/templates/docker_ec2_template.yaml b/cased/templates/docker_ec2_template.yaml new file mode 100644 index 0000000..79aace3 --- /dev/null +++ b/cased/templates/docker_ec2_template.yaml @@ -0,0 +1,83 @@ +name: Deploy to EC2 + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + branch: + description: 'Branch to deploy' + required: true + default: 'main' + target_name: + description: 'Target name' + required: true + default: 'prod' + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: {% raw %}${{ secrets.AWS_ACCESS_KEY_ID }}{% endraw %} + aws-secret-access-key: {% raw %}${{ secrets.AWS_SECRET_ACCESS_KEY }}{% endraw %} + aws-region: {% raw %}${{ secrets.AWS_REGION }}{% endraw %} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build, tag, and push image to Amazon ECR + env: + ECR_REGISTRY: {% raw %}${{ steps.login-ecr.outputs.registry }}{% endraw %} + ECR_REPOSITORY: {% raw %}${{ secrets.ECR_REPOSITORY }}{% endraw %} + IMAGE_TAG: {% raw %}${{ github.sha }}{% endraw %} + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG {{ config.docker.dockerfile_path }} + {% if config.docker.build_args %} + {% for arg in config.docker.build_args %} + --build-arg {{ arg }} \ + {% endfor %} + {% endif %} + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + + - name: Deploy to EC2 + env: + PRIVATE_KEY: {% raw %}${{ secrets.EC2_SSH_PRIVATE_KEY }}{% endraw %} + ECR_REGISTRY: {% raw %}${{ steps.login-ecr.outputs.registry }}{% endraw %} + EC2_PUBLIC_IP: {% raw %}${{ secrets.EC2_PUBLIC_IP }}{% endraw %} + ECR_REPOSITORY: {% raw %}${{ secrets.ECR_REPOSITORY }}{% endraw %} + IMAGE_TAG: {% raw %}${{ github.sha }}{% endraw %} + AWS_ACCOUNT_NUMBER: {% raw %}${{ secrets.AWS_ACCOUNT_NUMBER }}{% endraw %} + AWS_ACCESS_KEY_ID: {% raw %}${{ secrets.AWS_ACCESS_KEY_ID }}{% endraw %} + AWS_SECRET_ACCESS_KEY: {% raw %}${{ secrets.AWS_SECRET_ACCESS_KEY }}{% endraw %} + AWS_REGION: {% raw %}${{ secrets.AWS_REGION }}{% endraw %} + run: | + echo "$PRIVATE_KEY" > private_key && chmod 600 private_key + ssh -o StrictHostKeyChecking=no -i private_key $EC2_PUBLIC_IP " + export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID + export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY + aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_NUMBER.dkr.ecr.$AWS_REGION.amazonaws.com + docker pull $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + docker stop {{ config.docker.image_name }} || true + docker rm {{ config.docker.image_name }} || true + docker run -d --name {{ config.docker.image_name }} \ + {% if config.docker.ports %} + {% for port in config.docker.ports %} + -p {{ port }} \ + {% endfor %} + {% endif %} + {% if config.docker.environment %} + {% for env in config.docker.environment %} + -e {{ env }} \ + {% endfor %} + {% endif %} + $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + " diff --git a/cased/templates/non_docker_ec2_template.yaml b/cased/templates/non_docker_ec2_template.yaml new file mode 100644 index 0000000..10ebf33 --- /dev/null +++ b/cased/templates/non_docker_ec2_template.yaml @@ -0,0 +1,52 @@ +name: Deploy to EC2 + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + branch: + description: 'Branch to deploy' + required: true + default: 'main' + target_name: + description: 'Target name' + required: true + default: 'prod' + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '{{ config.environment.python_version }}' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install {{ config.environment.dependency_manager }} + {{ config.environment.dependency_manager }} install + + - name: Run tests + run: | + # Add your test commands here + + - name: Deploy to EC2 + env: + PRIVATE_KEY: {% raw %}${{ secrets.EC2_SSH_PRIVATE_KEY }}{% endraw %} + EC2_PUBLIC_IP: {% raw %}${{ secrets.EC2_PUBLIC_IP }}{% endraw %} + run: | + echo "$PRIVATE_KEY" > private_key && chmod 600 private_key + scp -i private_key -o StrictHostKeyChecking=no -r ./* ec2-user@$EC2_PUBLIC_IP:~/app + ssh -i private_key -o StrictHostKeyChecking=no ec2-user@$EC2_PUBLIC_IP ' + cd ~/app + {{ config.runtime.commands.stop }} + {{ config.runtime.commands.start }} + ' diff --git a/cased/utils/api.py b/cased/utils/api.py new file mode 100644 index 0000000..86d1b34 --- /dev/null +++ b/cased/utils/api.py @@ -0,0 +1,109 @@ +import requests +from rich.console import Console + +from cased.utils.config import load_config +from cased.utils.constants import CasedConstants +from cased.utils.exception import CasedAPIError + +console = Console() + + +# 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}, + ) + + +class CasedAPI: + def __init__(self): + configs = load_config(CasedConstants.ENV_FILE) + self.request_headers = { + "Authorization": f"Bearer {str(configs.get(CasedConstants.CASED_API_AUTH_KEY))}", + "Accept": "application/json", + } + + def _make_request(self, resource_name, method, url, **kwargs): + response = requests.request(method, url, headers=self.request_headers, **kwargs) + if response.status_code in [200, 201]: + return response.json() + else: + raise CasedAPIError( + f"Failed to fetch {resource_name} from {url}", + response.status_code, + response.json(), + ) + + def get_branches(self, project_name): + query_params = {"project_name": project_name} + return self._make_request( + resource_name="branches", + method="GET", + url=f"{CasedConstants.API_BASE_URL}/branches", + params=query_params, + ) + + def get_projects(self): + return self._make_request( + resource_name="projects", + method="GET", + url=f"{CasedConstants.BASE_URL}/projects", + ) + + def get_targets(self, project_name): + params = {"project_name": project_name} + return self._make_request( + resource_name="targets", + method="GET", + url=f"{CasedConstants.API_BASE_URL}/targets", + params=params, + ) + + def get_deployments(self, project_name, target_name=None): + params = {"project_name": project_name, "target_name": target_name} + return self._make_request( + resource_name="deployments", + method="GET", + url=f"{CasedConstants.API_BASE_URL}/deployments", + params=params, + ) + + def deploy_branch(self, project_name, branch_name, target_name): + json = { + "project_name": project_name, + "branch_name": branch_name, + "target_name": target_name, + } + return self._make_request( + resource_name="branch_deploy", + method="POST", + url=f"{CasedConstants.API_BASE_URL}/branch-deploys", + json=json, + ) + + 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, + ) + 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 new file mode 100644 index 0000000..24ff924 --- /dev/null +++ b/cased/utils/auth.py @@ -0,0 +1,60 @@ +from functools import wraps + +from rich.console import Console +from rich.panel import Panel + +from cased.utils.config import load_config +from cased.utils.constants import CasedConstants + +console = Console() + + +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..8acbae9 --- /dev/null +++ b/cased/utils/config.py @@ -0,0 +1,33 @@ +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..6d9ca69 --- /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/exception.py b/cased/utils/exception.py new file mode 100644 index 0000000..14e6ec1 --- /dev/null +++ b/cased/utils/exception.py @@ -0,0 +1,30 @@ +from typing import Any, Optional + + +class CasedAPIError(Exception): + def __init__( + self, message: str, status_code: Optional[int] = None, response_body: Any = None + ): + """ + Initialize the CasedAPIError. + + Args: + message (str): The error message. + status_code (Optional[int]): The HTTP status code of the failed request. + response_body (Any): The response body from the failed request. + """ + self.message = message + self.status_code = status_code + self.response_body = response_body + super().__init__(self.message) + + # TODO: make this specific based on status codes returned from our API, + # right now it is too generic. + def __str__(self): + """Return a string representation of the error.""" + error_msg = self.message + if self.status_code: + error_msg += f" (Status code: {self.status_code})" + if self.response_body: + error_msg += f"\nResponse body: {self.response_body}" + return error_msg diff --git a/cased/utils/git.py b/cased/utils/git.py new file mode 100644 index 0000000..2dcaed4 --- /dev/null +++ b/cased/utils/git.py @@ -0,0 +1,22 @@ +import subprocess + +from rich.console import Console + +console = Console() + + +def get_repo_name() -> str: + try: + result = subprocess.run( + ["git", "config", "--get", "remote.origin.url"], + capture_output=True, + text=True, + check=True, + ) + repo_url = result.stdout.strip() + return repo_url.split("/")[-1].replace(".git", "") + except subprocess.CalledProcessError: + console.print( + "[bold yellow]Warning: Unable to get repository name from git. Using 'unknown' as project name.[/bold yellow]" # noqa: E501 + ) + return "unknown" diff --git a/cased/utils/progress.py b/cased/utils/progress.py new file mode 100644 index 0000000..7da8735 --- /dev/null +++ b/cased/utils/progress.py @@ -0,0 +1,52 @@ +import sys +import time +from concurrent.futures import ThreadPoolExecutor, TimeoutError +from typing import Any, Callable + +from rich.console import Console +from rich.progress import Progress + +from cased.utils.exception import CasedAPIError + + +def run_process_with_status_bar( + process_func: Callable[[], Any], + description: str = "Processing...", + timeout: int = 10, + *args, + **kwargs, +) -> Any: + console = Console() + result = None + progress = Progress() + task = progress.add_task(f"[green]{description}", total=100) + + progress.start() + with ThreadPoolExecutor(max_workers=2) as executor: + future = executor.submit(process_func, *args, **kwargs) + start_time = time.time() + while not future.done() and time.time() - start_time < timeout: + elapsed = int(time.time() - start_time) + steps = (elapsed / timeout) * 100 + progress.update(task, completed=min(steps, 100)) + time.sleep(0.1) + + try: + result = future.result(timeout=0) # Non-blocking check + progress.update(task, completed=100, description="[bold green]Done!") + except TimeoutError: + console.print( + f"\n[bold red]Process timed out after {timeout} seconds. Please try again later." + ) + sys.exit(1) + except CasedAPIError as e: + console.print(f"\n[bold red]API Error: {e}") + sys.exit(1) + except Exception as _: + console.print( + "\n[bold red]An unexpected error occurred, please try again later." + ) + sys.exit(1) + + progress.stop() + return result diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..8b54266 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,717 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "ansicon" +version = "1.89.0" +description = "Python wrapper for loading Jason Hood's ANSICON" +optional = false +python-versions = "*" +files = [ + {file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"}, + {file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"}, +] + +[[package]] +name = "blessed" +version = "1.20.0" +description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." +optional = false +python-versions = ">=2.7" +files = [ + {file = "blessed-1.20.0-py2.py3-none-any.whl", hash = "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058"}, + {file = "blessed-1.20.0.tar.gz", hash = "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680"}, +] + +[package.dependencies] +jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""} +six = ">=1.9.0" +wcwidth = ">=0.1.4" + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {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" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "editor" +version = "1.6.6" +description = "🖋 Open the default text editor 🖋" +optional = false +python-versions = ">=3.8" +files = [ + {file = "editor-1.6.6-py3-none-any.whl", hash = "sha256:e818e6913f26c2a81eadef503a2741d7cca7f235d20e217274a009ecd5a74abf"}, + {file = "editor-1.6.6.tar.gz", hash = "sha256:bb6989e872638cd119db9a4fce284cd8e13c553886a1c044c6b8d8a160c871f8"}, +] + +[package.dependencies] +runs = "*" +xmod = "*" + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {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 (>=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.1" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {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] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {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" +description = "Collection of common interactive command line user interfaces, based on Inquirer.js" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "inquirer-3.4.0-py3-none-any.whl", hash = "sha256:bb0ec93c833e4ce7b51b98b1644b0a4d2bb39755c39787f6a504e4fee7a11b60"}, + {file = "inquirer-3.4.0.tar.gz", hash = "sha256:8edc99c076386ee2d2204e5e3653c2488244e82cb197b2d498b3c1b5ffb25d0b"}, +] + +[package.dependencies] +blessed = ">=1.19.0" +editor = ">=1.6.0" +readchar = ">=4.2.0" + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jinxed" +version = "1.3.0" +description = "Jinxed Terminal Library" +optional = false +python-versions = "*" +files = [ + {file = "jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5"}, + {file = "jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf"}, +] + +[package.dependencies] +ansicon = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "platformdirs" +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.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +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" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.6.2" +files = [ + {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] +wcwidth = "*" + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "questionary" +version = "2.0.1" +description = "Python library to build pretty command line user prompts ⭐️" +optional = false +python-versions = ">=3.8" +files = [ + {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,<=3.0.36" + +[[package]] +name = "readchar" +version = "4.2.0" +description = "Library to easily read single chars and key strokes" +optional = false +python-versions = ">=3.8" +files = [ + {file = "readchar-4.2.0-py3-none-any.whl", hash = "sha256:2a587a27c981e6d25a518730ad4c88c429c315439baa6fda55d7a8b3ac4cb62a"}, + {file = "readchar-4.2.0.tar.gz", hash = "sha256:44807cbbe377b72079fea6cba8aa91c809982d7d727b2f0dbb2d1a8084914faa"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "12.6.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.6.3,<4.0.0" +files = [ + {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, + {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, +] + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "ruff" +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.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]] +name = "runs" +version = "1.2.2" +description = "🏃 Run a block of text as a subprocess 🏃" +optional = false +python-versions = ">=3.8" +files = [ + {file = "runs-1.2.2-py3-none-any.whl", hash = "sha256:0980dcbc25aba1505f307ac4f0e9e92cbd0be2a15a1e983ee86c24c87b839dfd"}, + {file = "runs-1.2.2.tar.gz", hash = "sha256:9dc1815e2895cfb3a48317b173b9f1eac9ba5549b36a847b5cc60c3bf82ecef1"}, +] + +[package.dependencies] +xmod = "*" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {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)"] +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.6" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, + {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "xmod" +version = "1.8.1" +description = "🌱 Turn any object into a module 🌱" +optional = false +python-versions = ">=3.8" +files = [ + {file = "xmod-1.8.1-py3-none-any.whl", hash = "sha256:a24e9458a4853489042522bdca9e50ee2eac5ab75c809a91150a8a7f40670d48"}, + {file = "xmod-1.8.1.tar.gz", hash = "sha256:38c76486b9d672c546d57d8035df0beb7f4a9b088bc3fb2de5431ae821444377"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "619c31e510dbccccf0ad264bdac0a7e7af5c545009afeba4c167e2699ded5470" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3e0e8bf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,34 @@ +[tool.poetry] +name = "cased" +version = "0.1.0" +description = "A CLI tool for managing deployments and branches" +authors = ["Cased "] +readme = "README.md" +repository = "https://github.com/cased/cli" +packages = [{include = "cased"}] + +[tool.poetry.dependencies] +python = "^3.12" +click = "*" +requests = "*" +rich = "*" +questionary = "*" +python-dateutil = "*" +pyyaml = "^6.0.2" +inquirer = "^3.4.0" +jinja2 = "^3.1.4" + +[tool.poetry.scripts] +cased = "cased.cli:cli" + +[tool.poetry.group.dev.dependencies] +pre-commit = "^3.8.0" +ruff = "^0.6.2" +isort = "^5.13.2" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.urls] +"Homepage" = "https://github.com/cased/csd"