88"""
99
1010import logging
11+ import os
12+ import subprocess
1113import sys
1214import time
1315from typing import Optional
1416
17+ import boto3
1518import click
1619from rich .console import Console
1720from rich .live import Live
3134
3235console = Console ()
3336
37+
38+ def _build_from_local_code (from_code_dir : str , region : str , stack_name : str ) -> tuple :
39+ """
40+ Build project from local code using publish.py
41+
42+ Args:
43+ from_code_dir: Path to project root directory
44+ region: AWS region
45+ stack_name: CloudFormation stack name (unused but kept for signature compatibility)
46+
47+ Returns:
48+ Tuple of (template_path, None) on success
49+
50+ Raises:
51+ SystemExit: On build failure
52+ """
53+ # Verify publish.py exists
54+ publish_script = os .path .join (from_code_dir , "publish.py" )
55+ if not os .path .isfile (publish_script ):
56+ console .print (f"[red]✗ Error: publish.py not found in { from_code_dir } [/red]" )
57+ console .print (
58+ "[yellow]Tip: --from-code should point to the project root directory[/yellow]"
59+ )
60+ sys .exit (1 )
61+
62+ # Get AWS account ID
63+ try :
64+ sts = boto3 .client ("sts" , region_name = region )
65+ account_id = sts .get_caller_identity ()["Account" ]
66+ except Exception as e :
67+ console .print (f"[red]✗ Error: Failed to get AWS account ID: { e } [/red]" )
68+ sys .exit (1 )
69+
70+ # Set parameters for publish.py
71+ cfn_bucket_basename = f"idp-accelerator-artifacts-{ account_id } "
72+ cfn_prefix = "idp-cli"
73+
74+ console .print ("[bold cyan]Building project from source...[/bold cyan]" )
75+ console .print (f"[dim]Bucket: { cfn_bucket_basename } [/dim]" )
76+ console .print (f"[dim]Prefix: { cfn_prefix } [/dim]" )
77+ console .print (f"[dim]Region: { region } [/dim]" )
78+ console .print ()
79+
80+ # Build command
81+ cmd = [
82+ sys .executable , # Use same Python interpreter
83+ publish_script ,
84+ cfn_bucket_basename ,
85+ cfn_prefix ,
86+ region ,
87+ ]
88+
89+ console .print (f"[dim]Running: { ' ' .join (cmd )} [/dim]" )
90+ console .print ()
91+
92+ # Run with streaming output
93+ try :
94+ process = subprocess .Popen (
95+ cmd ,
96+ stdout = subprocess .PIPE ,
97+ stderr = subprocess .STDOUT ,
98+ text = True ,
99+ bufsize = 1 ,
100+ cwd = from_code_dir ,
101+ )
102+
103+ # Stream output line by line
104+ for line in process .stdout :
105+ # Print each line immediately (preserve formatting from publish.py)
106+ print (line , end = "" )
107+
108+ process .wait ()
109+
110+ if process .returncode != 0 :
111+ console .print ("[red]✗ Build failed. See output above for details.[/red]" )
112+ sys .exit (1 )
113+
114+ except Exception as e :
115+ console .print (f"[red]✗ Error running publish.py: { e } [/red]" )
116+ sys .exit (1 )
117+
118+ # Verify template was created
119+ template_path = os .path .join (from_code_dir , ".aws-sam" , "idp-main.yaml" )
120+ if not os .path .isfile (template_path ):
121+ console .print (
122+ f"[red]✗ Error: Built template not found at { template_path } [/red]"
123+ )
124+ console .print (
125+ "[yellow]The build may have failed or the template was not generated.[/yellow]"
126+ )
127+ sys .exit (1 )
128+
129+ console .print ()
130+ console .print (f"[green]✓ Build complete. Using template: { template_path } [/green]" )
131+ console .print ()
132+
133+ return template_path , None
134+
135+
34136# Region-specific template URLs
35137TEMPLATE_URLS = {
36138 "us-west-2" : "https://s3.us-west-2.amazonaws.com/aws-ml-blog-us-west-2/artifacts/genai-idp/idp-main.yaml" ,
40142
41143
42144@click .group ()
43- @click .version_option (version = "0.4.2 " )
145+ @click .version_option (version = "0.4.4 " )
44146def cli ():
45147 """
46148 IDP CLI - Batch document processing for IDP Accelerator
@@ -64,6 +166,11 @@ def cli():
64166@click .option (
65167 "--admin-email" , help = "Admin user email address (required for new stacks)"
66168)
169+ @click .option (
170+ "--from-code" ,
171+ type = click .Path (exists = True , file_okay = False , dir_okay = True ),
172+ help = "Deploy from local code by building with publish.py (path to project root)" ,
173+ )
67174@click .option (
68175 "--template-url" ,
69176 help = "URL to CloudFormation template in S3 (default: auto-selected based on region)" ,
@@ -93,11 +200,15 @@ def cli():
93200)
94201@click .option ("--parameters" , help = "Additional parameters as key=value,key2=value2" )
95202@click .option ("--wait" , is_flag = True , help = "Wait for stack creation to complete" )
203+ @click .option (
204+ "--no-rollback" , is_flag = True , help = "Disable rollback on stack creation failure"
205+ )
96206@click .option ("--region" , help = "AWS region (optional)" )
97207def deploy (
98208 stack_name : str ,
99209 pattern : str ,
100210 admin_email : str ,
211+ from_code : Optional [str ],
101212 template_url : str ,
102213 max_concurrent : int ,
103214 log_level : str ,
@@ -106,6 +217,7 @@ def deploy(
106217 custom_config : Optional [str ],
107218 parameters : Optional [str ],
108219 wait : bool ,
220+ no_rollback : bool ,
109221 region : Optional [str ],
110222):
111223 """
@@ -119,9 +231,15 @@ def deploy(
119231 # Create new stack with Pattern 2
120232 idp-cli deploy --stack-name my-idp --pattern pattern-2 --admin-email user@example.com
121233
122- # Update existing stack with local config file (NEW!)
234+ # Deploy from local code (NEW!)
235+ idp-cli deploy --stack-name my-idp --from-code . --pattern pattern-2 --admin-email user@example.com --wait
236+
237+ # Update existing stack with local config file
123238 idp-cli deploy --stack-name my-idp --custom-config ./my-config.yaml
124239
240+ # Update existing stack from local code
241+ idp-cli deploy --stack-name my-idp --from-code . --wait
242+
125243 # Update existing stack with custom settings
126244 idp-cli deploy --stack-name my-idp --max-concurrent 200 --wait
127245
@@ -131,6 +249,13 @@ def deploy(
131249 --parameters "DataRetentionInDays=90,ErrorThreshold=5"
132250 """
133251 try :
252+ # Validate mutually exclusive options
253+ if from_code and template_url :
254+ console .print (
255+ "[red]✗ Error: Cannot specify both --from-code and --template-url[/red]"
256+ )
257+ sys .exit (1 )
258+
134259 # Auto-detect region if not provided
135260 if not region :
136261 import boto3
@@ -142,8 +267,15 @@ def deploy(
142267 "Region could not be determined. Please specify --region or configure AWS_DEFAULT_REGION"
143268 )
144269
270+ # Handle deployment from local code
271+ template_path = None
272+ if from_code :
273+ template_path , template_url = _build_from_local_code (
274+ from_code , region , stack_name
275+ )
276+
145277 # Determine template URL (user-provided takes precedence)
146- if not template_url :
278+ elif not template_url :
147279 if region in TEMPLATE_URLS :
148280 template_url = TEMPLATE_URLS [region ]
149281 console .print (f"[bold]Using template for region: { region } [/bold]" )
@@ -224,12 +356,24 @@ def deploy(
224356
225357 # Deploy stack
226358 with console .status ("[bold green]Deploying stack..." ):
227- result = deployer .deploy_stack (
228- stack_name = stack_name ,
229- template_url = template_url ,
230- parameters = cfn_parameters ,
231- wait = wait ,
232- )
359+ if template_path :
360+ # Deploy from local template (built from code)
361+ result = deployer .deploy_stack (
362+ stack_name = stack_name ,
363+ template_path = template_path ,
364+ parameters = cfn_parameters ,
365+ wait = wait ,
366+ no_rollback = no_rollback ,
367+ )
368+ else :
369+ # Deploy from template URL
370+ result = deployer .deploy_stack (
371+ stack_name = stack_name ,
372+ template_url = template_url ,
373+ parameters = cfn_parameters ,
374+ wait = wait ,
375+ no_rollback = no_rollback ,
376+ )
233377
234378 # Show results
235379 # Success if operation completed successfully OR was successfully initiated
0 commit comments