diff --git a/.ci-pre-commit-config.yaml b/.ci-pre-commit-config.yaml index 7eb4e3f..9797595 100644 --- a/.ci-pre-commit-config.yaml +++ b/.ci-pre-commit-config.yaml @@ -2,7 +2,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.3.0 hooks: - - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace exclude: "^comet/core/migrations/.*" diff --git a/cased/cli.py b/cased/cli.py index ccba2e8..ec7c2b4 100644 --- a/cased/cli.py +++ b/cased/cli.py @@ -1,9 +1,13 @@ 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 +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + @click.group() def cli(): @@ -17,6 +21,8 @@ def cli(): cli.add_command(deploy) +cli.add_command(init) +cli.add_command(build) cli.add_command(login) cli.add_command(logout) cli.add_command(deployments) diff --git a/cased/commands/build.py b/cased/commands/build.py new file mode 100644 index 0000000..9adf411 --- /dev/null +++ b/cased/commands/build.py @@ -0,0 +1,176 @@ +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 API_BASE_URL, create_secrets +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) + 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 {API_BASE_URL}/secrets/{project_name} to update the secrets. + 3. Commit the changes to your repository, and the workflow will be triggered. + 4. Go to {API_BASE_URL}/deployments/ to monitor the deployment status. + """, # 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/init.py b/cased/commands/init.py new file mode 100644 index 0000000..bdbedb2 --- /dev/null +++ b/cased/commands/init.py @@ -0,0 +1,221 @@ +import os + +import click +import inquirer +import yaml +from rich.console import Console +from rich.panel import Panel + +from cased.utils.progress import run_process_with_status_bar + +console = Console() + + +@click.command() +def init(): + """Initialize a new project configuration.""" + 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/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 index c63aaa8..15d3178 100644 --- a/cased/utils/api.py +++ b/cased/utils/api.py @@ -2,6 +2,9 @@ import click import requests +from rich.console import Console + +console = Console() API_BASE_URL = os.environ.get( "CASED_API_BASE_URL", default="https://app.cased.com/api/v1" @@ -58,3 +61,29 @@ def deploy_branch(branch_name, target_name): ) else: click.echo("Deployment failed. Please check your input and try again.") + + +def create_secrets(project_name: str, secrets: list): + payload = { + "storage_destination": "github_repository", + "keys": [{"name": secret, "type": "credentials"} for secret in secrets], + } + response = requests.post( + f"{API_BASE_URL}/api/v1/secrets/{project_name}/setup", + json=payload, + headers=REQUEST_HEADERS, + ) + if response.status_code == 201: + console.print("[green]Secrets setup successful![/green]") + console.print( + f"Please go to {API_BASE_URL}/secrets/{project_name} to update these secrets." # noqa: E501 + ) + 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/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/poetry.lock b/poetry.lock index cf9978d..7fb981f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,31 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# 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" @@ -157,6 +184,21 @@ files = [ {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.15.4" @@ -198,6 +240,22 @@ files = [ {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, ] +[[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" @@ -212,6 +270,37 @@ files = [ [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 = "markdown-it-py" version = "2.2.0" @@ -236,6 +325,75 @@ profiling = ["gprof2dot"] rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "markupsafe" +version = "2.1.5" +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 = "mdurl" version = "0.1.2" @@ -414,6 +572,17 @@ prompt_toolkit = ">=2.0,<4.0" [package.extras] docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)"] +[[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.31.0" @@ -480,6 +649,20 @@ files = [ {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] +[[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" @@ -539,7 +722,18 @@ files = [ {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.9" -content-hash = "bc15e0ad22317d4a600f0e4102ed1820f01f47db0270acf53cb1d8c92c359d47" +content-hash = "c2f24afce3179803aa334bbc58e86bdd374a3490ce83bc1eccaa4b2f13a6756c" diff --git a/pyproject.toml b/pyproject.toml index 3c06e33..2a3e987 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,9 @@ requests = "*" rich = "*" questionary = "*" python-dateutil = "*" +pyyaml = "^6.0.2" +inquirer = "^3.4.0" +jinja2 = "^3.1.4" [tool.poetry.scripts] cased = "cased.cli:cli"