11from pathlib import Path
22from subprocess import run
33from typing import List
4- import time
4+ from typing_extensions import Annotated
55from datetime import timedelta , datetime
66
7+ import json
78import logging
9+ import time
810import typer
911
12+ from aws_doc_sdk_examples_tools .agent .shared_constants import BATCH_PREFIX
1013from aws_doc_sdk_examples_tools .agent .make_prompts import make_prompts
1114from aws_doc_sdk_examples_tools .agent .process_ailly_files import process_ailly_files
1215from aws_doc_sdk_examples_tools .agent .update_doc_gen import update_doc_gen
1316from aws_doc_sdk_examples_tools .yaml_writer import prepare_write , write_many
1417
1518logging .basicConfig (
16- level = logging .INFO , filename = f"lliam-run-{ datetime .now ()} .log" , filemode = "w"
19+ level = logging .INFO ,
20+ filename = f"lliam-run-{ datetime .now ().strftime ('%Y%m%d_%H%M%S' )} .log" ,
21+ filemode = "w" ,
1722)
1823logger = logging .getLogger (__name__ )
1924
2025app = typer .Typer ()
2126
2227AILLY_DIR = ".ailly_iam_policy"
2328AILLY_DIR_PATH = Path (AILLY_DIR )
24- IAM_UPDATES_PATH = AILLY_DIR_PATH / "iam_updates.json"
2529
2630
2731def format_duration (seconds : float ) -> str :
@@ -30,72 +34,160 @@ def format_duration(seconds: float) -> str:
3034
3135
3236@app .command ()
33- def update (
34- iam_tributary_root : str ,
35- system_prompts : List [str ] = [],
36- skip_generation : bool = False ,
37+ def create_prompts (iam_tributary_root : str , system_prompts : List [str ] = []):
38+ doc_gen_root = Path (iam_tributary_root )
39+
40+ make_prompts (
41+ doc_gen_root = doc_gen_root ,
42+ system_prompts = system_prompts ,
43+ out_dir = AILLY_DIR_PATH ,
44+ language = "IAMPolicyGrammar" ,
45+ )
46+
47+
48+ def parse_batch_names (batch_nums_str : str ) -> List [str ]:
49+ """
50+ Parse batch numbers from a string.
51+ """
52+ if not batch_nums_str :
53+ return []
54+
55+ result = []
56+ parts = batch_nums_str .split ("," )
57+
58+ for part in parts :
59+ part = part .strip ()
60+ if ".." in part :
61+ start , end = part .split (".." )
62+ start_num = int (start .strip ())
63+ end_num = int (end .strip ())
64+ result .extend (range (start_num , end_num + 1 ))
65+ else :
66+ result .append (int (part ))
67+
68+ batch_nums = sorted (list (set (result )))
69+ return map (lambda b : f"{ BATCH_PREFIX } { b :03} " , batch_nums )
70+
71+
72+ def resolve_requested_batches (batch_names : List [str ]) -> List [Path ]:
73+ if not batch_names :
74+ batch_paths = [
75+ p
76+ for p in AILLY_DIR_PATH .iterdir ()
77+ if p .is_dir () and p .name .startswith (BATCH_PREFIX )
78+ ]
79+
80+ return batch_paths
81+
82+ batch_paths = []
83+
84+ for batch_name in batch_names :
85+ batch_path = Path (AILLY_DIR_PATH / batch_name )
86+ if not batch_path .exists ():
87+ raise FileNotFoundError (batch_path )
88+ if not batch_path .is_dir ():
89+ raise NotADirectoryError (batch_path )
90+ batch_paths .append (batch_path )
91+
92+ return batch_paths
93+
94+
95+ @app .command ()
96+ def run_ailly (
97+ batch_nums : Annotated [
98+ str ,
99+ typer .Option (
100+ help = "Batch numbers to process (e.g., '33', '33..35', '33,35,37')"
101+ ),
102+ ] = None ,
37103) -> None :
38104 """
39- Generate new IAM policy metadata for a tributary.
105+ Run ailly to generate IAM policy content and process the results.
106+ If batch_nums is specified, only those batches will be processed.
107+ If batch_nums is omitted, all batches will be processed.
40108 """
41- doc_gen_root = Path (iam_tributary_root )
109+ requested_batches = parse_batch_names (batch_nums )
110+ resolved_batches = resolve_requested_batches (requested_batches )
42111
43- if not skip_generation :
44- make_prompts (
45- doc_gen_root = doc_gen_root ,
46- system_prompts = system_prompts ,
47- out_dir = AILLY_DIR_PATH ,
48- language = "IAMPolicyGrammar" ,
112+ if resolved_batches :
113+ total_start_time = time .time ()
114+
115+ for batch in resolved_batches :
116+ run_ailly_single_batch (batch )
117+
118+ total_end_time = time .time ()
119+ total_duration = total_end_time - total_start_time
120+ num_batches = len (resolved_batches )
121+ logger .info (
122+ f"[TIMECHECK] { num_batches } batches took { format_duration (total_duration )} to run"
49123 )
50124
51- batch_dirs = [
52- d .name
53- for d in AILLY_DIR_PATH .iterdir ()
54- if d .is_dir () and d .name .startswith ("batch_" )
55- ]
56125
57- if batch_dirs :
58- total_start_time = time .time ()
59-
60- for batch_dir in sorted (batch_dirs ):
61- batch_start_time = time .time ()
62-
63- cmd = [
64- "ailly" ,
65- "--max-depth" ,
66- "10" ,
67- "--root" ,
68- AILLY_DIR ,
69- str (batch_dir ),
70- ]
71- logger .info (f"Running { cmd } " )
72- run (cmd )
73-
74- batch_end_time = time .time ()
75- batch_duration = batch_end_time - batch_start_time
76- batch_num = batch_dir .replace ("batch_" , "" )
77- logger .info (
78- f"[TIMECHECK] Batch { batch_num } took { format_duration (batch_duration )} to run"
79- )
80-
81- total_end_time = time .time ()
82- total_duration = total_end_time - total_start_time
83- num_batches = len (batch_dirs )
84- logger .info (
85- f"[TIMECHECK] { num_batches } batches took { format_duration (total_duration )} to run"
86- )
87-
88- logger .info ("Processing generated content" )
89- process_ailly_files (
90- input_dir = str (AILLY_DIR_PATH ), output_file = str (IAM_UPDATES_PATH )
126+ @app .command ()
127+ def update_reservoir (
128+ iam_tributary_root : str ,
129+ batch_nums : Annotated [
130+ str ,
131+ typer .Option (
132+ help = "Batch numbers to process (e.g., '33', '33..35', '33,35,37')"
133+ ),
134+ ] = None ,
135+ ) -> None :
136+ """
137+ Update the doc_gen reservoir with processed IAM policy updates.
138+ If batch_nums is specified, only those batches will be processed.
139+ If batch_nums is omitted, all available update files will be processed.
140+ """
141+ doc_gen_root = Path (iam_tributary_root )
142+ batch_names = parse_batch_names (batch_nums ) if batch_nums else None
143+
144+ update_files = (
145+ [AILLY_DIR_PATH / f"updates_{ batch } .json" for batch in batch_names ]
146+ if batch_names
147+ else list (AILLY_DIR_PATH .glob (f"updates_{ BATCH_PREFIX } *.json" ))
91148 )
92149
93- doc_gen = update_doc_gen (
94- doc_gen_root = doc_gen_root , iam_updates_path = IAM_UPDATES_PATH
150+ if not update_files :
151+ logger .warning ("No IAM update files found to process" )
152+ return
153+
154+ for update_file in sorted (update_files ):
155+ if update_file .exists ():
156+ logger .info (f"Processing updates from { update_file .name } " )
157+ updates = json .loads (update_file .read_text ())
158+
159+ doc_gen = update_doc_gen (doc_gen_root = doc_gen_root , updates = updates )
160+
161+ writes = prepare_write (doc_gen .examples )
162+ write_many (doc_gen_root , writes )
163+ else :
164+ logger .warning (f"Update file not found: { update_file } " )
165+
166+
167+ def run_ailly_single_batch (batch : Path ) -> None :
168+ """Run ailly and process files for a single batch."""
169+ batch_start_time = time .time ()
170+ iam_updates_path = AILLY_DIR_PATH / f"updates_{ batch .name } .json"
171+
172+ cmd = [
173+ "ailly" ,
174+ "--max-depth" ,
175+ "10" ,
176+ "--root" ,
177+ str (AILLY_DIR_PATH ),
178+ batch .name ,
179+ ]
180+ logger .info (f"Running { cmd } " )
181+ run (cmd )
182+
183+ batch_end_time = time .time ()
184+ batch_duration = batch_end_time - batch_start_time
185+ logger .info (
186+ f"[TIMECHECK] { batch .name } took { format_duration (batch_duration )} to run"
95187 )
96188
97- writes = prepare_write ( doc_gen . examples )
98- write_many ( doc_gen_root , writes )
189+ logger . info ( f"Processing generated content for { batch . name } " )
190+ process_ailly_files ( input_dir = batch , output_file = iam_updates_path )
99191
100192
101193if __name__ == "__main__" :
0 commit comments