diff --git a/preswald/cli.py b/preswald/cli.py index c25f2c1..8122725 100644 --- a/preswald/cli.py +++ b/preswald/cli.py @@ -10,7 +10,7 @@ from preswald.deploy import deploy as deploy_app from preswald.deploy import stop as stop_app from preswald.main import start_server -from preswald.utils import configure_logging, read_port_from_config, read_template +from preswald.utils import configure_logging, read_template, get_project_slug, generate_slug, read_port_from_config # Create a temporary directory for IPC @@ -40,6 +40,9 @@ def init(name): os.makedirs(os.path.join(name, "images"), exist_ok=True) os.makedirs(os.path.join(name, "data"), exist_ok=True) + # Generate a unique slug for the project + project_slug = generate_slug(name) + # Copy default branding files from package resources import shutil @@ -62,10 +65,16 @@ def init(name): for file_name, template_name in file_templates.items(): content = read_template(template_name) + + # Replace the default slug in preswald.toml with the generated one + if file_name == "preswald.toml": + content = content.replace('slug = "preswald-project"', f'slug = "{project_slug}"') + with open(os.path.join(name, file_name), "w") as f: f.write(content) click.echo(f"Initialized a new Preswald project in '{name}/' 🎉!") + click.echo(f"Project slug: {project_slug}") except Exception as e: click.echo(f"Error initializing project: {e} ❌") @@ -161,6 +170,14 @@ def deploy(script, target, port, log_level, github, api_key): port = read_port_from_config(config_path=config_path, port=port) if target == "structured": + # Validate project slug before deployment + try: + project_slug = get_project_slug(config_path) + click.echo(f"Using project slug: {project_slug}") + except Exception as e: + click.echo(click.style(f"Error: {str(e)} ❌", fg="red")) + return + click.echo("Starting production deployment... 🚀") try: for status_update in deploy_app( @@ -203,7 +220,8 @@ def deploy(script, target, port, log_level, github, api_key): click.echo(click.style(success_message, fg="green")) except Exception as e: - click.echo(f"Error deploying app: {e} ❌") + click.echo(click.style(f"Deployment failed: {e!s} ❌", fg="red")) + sys.exit(1) @cli.command() diff --git a/preswald/deploy.py b/preswald/deploy.py index 3e4c250..193924f 100644 --- a/preswald/deploy.py +++ b/preswald/deploy.py @@ -11,7 +11,7 @@ from datetime import datetime import io -from preswald.utils import read_template +from preswald.utils import read_template, get_project_slug logger = logging.getLogger(__name__) @@ -243,8 +243,20 @@ def deploy_to_prod(script_path: str, port: int = 8501, github_username: str = No """ script_path = os.path.abspath(script_path) script_dir = Path(script_path).parent + config_path = script_dir / "preswald.toml" env_file = script_dir / '.env.structured' + # Get project slug from preswald.toml + try: + project_slug = get_project_slug(config_path) + except Exception as e: + yield { + 'status': 'error', + 'message': f'Failed to get project slug: {str(e)}', + 'timestamp': datetime.now().isoformat() + } + raise Exception(f"Failed to get project slug: {str(e)}") + if not env_file.exists(): # Use provided credentials or get from user input if not github_username: @@ -254,14 +266,10 @@ def deploy_to_prod(script_path: str, port: int = 8501, github_username: str = No else: structured_cloud_api_key = api_key - # Generate a unique app ID (using timestamp) - app_id = f"app_{int(datetime.now().timestamp())}" - # Create and populate .env.structured file with open(env_file, 'w') as f: f.write(f"GITHUB_USERNAME={github_username}\n") - f.write(f"STRUCTURED_CLOUD_API_KEY={structured_cloud_api_key}\n") - f.write(f"APP_ID={app_id}\n") + f.write(f"STRUCTURED_CLOUD_API_KEY={structured_cloud_api_key}\n") else: # Read credentials from existing env file if not provided via CLI credentials = {} @@ -272,7 +280,6 @@ def deploy_to_prod(script_path: str, port: int = 8501, github_username: str = No github_username = github_username or credentials['GITHUB_USERNAME'] structured_cloud_api_key = api_key or credentials['STRUCTURED_CLOUD_API_KEY'] - app_id = credentials['APP_ID'] # Create a temporary zip file zip_buffer = io.BytesIO() @@ -309,7 +316,7 @@ def deploy_to_prod(script_path: str, port: int = 8501, github_username: str = No data={ 'github_username': github_username, 'structured_cloud_api_key': structured_cloud_api_key, - 'app_id': app_id, + 'project_slug': project_slug, 'git_repo_name': git_repo_name, }, stream=True @@ -332,7 +339,6 @@ def deploy_to_prod(script_path: str, port: int = 8501, github_username: str = No } raise Exception(f"Production deployment failed: {str(e)}") - def deploy_to_gcp(script_path: str, port: int = 8501) -> str: """ Deploy a Preswald app to Google Cloud Run. @@ -573,8 +579,15 @@ def stop_structured_deployment(script_path: str) -> dict: dict: Status of the stop operation """ script_dir = Path(script_path).parent + config_path = script_dir / "preswald.toml" env_file = script_dir / '.env.structured' + # Get project slug from preswald.toml + try: + project_slug = get_project_slug(config_path) + except Exception as e: + raise Exception(f"Failed to get project slug: {str(e)}") + if not env_file.exists(): raise Exception("No deployment found. The .env.structured file is missing.") @@ -587,7 +600,6 @@ def stop_structured_deployment(script_path: str) -> dict: github_username = credentials['GITHUB_USERNAME'] structured_cloud_api_key = credentials['STRUCTURED_CLOUD_API_KEY'] - app_id = credentials['APP_ID'] try: response = requests.post( @@ -595,7 +607,7 @@ def stop_structured_deployment(script_path: str) -> dict: json={ 'github_username': github_username, 'structured_cloud_api_key': structured_cloud_api_key, - 'app_id': app_id + 'project_slug': project_slug } ) response.raise_for_status() @@ -616,8 +628,15 @@ def get_structured_deployments(script_path: str) -> dict: dict: Deployment information including user, organization, and deployments list """ script_dir = Path(script_path).parent + config_path = script_dir / "preswald.toml" env_file = script_dir / '.env.structured' + # Get project slug from preswald.toml + try: + project_slug = get_project_slug(config_path) + except Exception as e: + raise Exception(f"Failed to get project slug: {str(e)}") + if not env_file.exists(): raise Exception("No deployment found. The .env.structured file is missing.") @@ -630,7 +649,6 @@ def get_structured_deployments(script_path: str) -> dict: github_username = credentials['GITHUB_USERNAME'] structured_cloud_api_key = credentials['STRUCTURED_CLOUD_API_KEY'] - app_id = credentials['APP_ID'] try: response = requests.post( @@ -638,7 +656,7 @@ def get_structured_deployments(script_path: str) -> dict: json={ 'github_username': github_username, 'structured_cloud_api_key': structured_cloud_api_key, - 'app_id': app_id + 'project_slug': project_slug } ) response.raise_for_status() diff --git a/preswald/templates/preswald.toml.template b/preswald/templates/preswald.toml.template index cc4a63c..db1cf13 100644 --- a/preswald/templates/preswald.toml.template +++ b/preswald/templates/preswald.toml.template @@ -2,6 +2,7 @@ title = "Preswald Project" version = "0.1.0" port = 8501 +slug = "preswald-project" # Required: Unique identifier for your project [branding] name = "Preswald Project" diff --git a/preswald/utils.py b/preswald/utils.py index 5d276ce..41be9f6 100644 --- a/preswald/utils.py +++ b/preswald/utils.py @@ -4,7 +4,9 @@ import pkg_resources import toml - +import logging +import re +import random def read_template(template_name): """Read content from a template file.""" @@ -69,3 +71,43 @@ def configure_logging(config_path: Optional[str] = None, level: Optional[str] = logger.debug(f"Logging configured with level {log_config['level']}") return log_config["level"] + +def validate_slug(slug: str) -> bool: + pattern = r'^[a-z0-9][a-z0-9-]*[a-z0-9]$' + return bool(re.match(pattern, slug)) and len(slug) >= 3 and len(slug) <= 63 + + +def get_project_slug(config_path: str) -> str: + if not os.path.exists(config_path): + raise Exception(f"Config file not found at: {config_path}") + + try: + config = toml.load(config_path) + if "project" not in config: + raise Exception("Missing [project] section in preswald.toml") + + if "slug" not in config["project"]: + raise Exception("Missing required field 'slug' in [project] section") + + slug = config["project"]["slug"] + if not validate_slug(slug): + raise Exception( + "Invalid slug format. Slug must be 3-63 characters long, " + "contain only lowercase letters, numbers, and hyphens, " + "and must start and end with a letter or number." + ) + + return slug + + except Exception as e: + raise Exception(f"Error reading project slug: {str(e)}") + + +def generate_slug(base_name: str) -> str: + base_slug = re.sub(r'[^a-zA-Z0-9]+', '-', base_name.lower()).strip('-') + random_number = random.randint(100000, 999999) + slug = f"{base_slug}-{random_number}" + if not validate_slug(slug): + slug = f"preswald-{random_number}" + + return slug