Coverage for cli/commands/capacity_cmd.py: 91.06%

700 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-30 21:22 +0000

1"""Capacity checking commands.""" 

2 

3import sys 

4from typing import Any 

5 

6import click 

7from botocore.exceptions import ClientError 

8 

9from gco.bedrock import BEDROCK_FTU_REMEDIATION, is_bedrock_ftu_form_error 

10 

11from ..capacity import get_capacity_checker 

12from ..config import GCOConfig 

13from ..output import format_capacity_table, get_output_formatter 

14 

15pass_config = click.make_pass_decorator(GCOConfig, ensure=True) 

16 

17 

18@click.group() 

19@pass_config 

20def capacity(config: Any) -> None: 

21 """Check EC2 capacity availability.""" 

22 pass 

23 

24 

25@capacity.command("check") 

26@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

27@click.option("--region", "-r", required=True, help="AWS region") 

28@click.option( 

29 "--type", 

30 "-t", 

31 "capacity_type", 

32 type=click.Choice(["spot", "on-demand", "both"]), 

33 default="both", 

34 help="Capacity type to check", 

35) 

36@click.option( 

37 "--enrich-historical", 

38 is_flag=True, 

39 help="Append historical capacity context (requires historical.enabled)", 

40) 

41@pass_config 

42def check_capacity( 

43 config: Any, 

44 instance_type: Any, 

45 region: Any, 

46 capacity_type: Any, 

47 enrich_historical: Any, 

48) -> None: 

49 """Check capacity availability for an instance type. 

50 

51 Provides estimates based on spot price history and availability patterns. 

52 """ 

53 formatter = get_output_formatter(config) 

54 checker = get_capacity_checker(config) 

55 

56 try: 

57 estimates = checker.estimate_capacity(instance_type, region, capacity_type) 

58 

59 if config.output_format == "table": 

60 print(format_capacity_table(estimates)) 

61 else: 

62 formatter.print(estimates) 

63 

64 if enrich_historical: 

65 _print_historical_enrichment(formatter, instance_type, region) 

66 

67 except Exception as e: 

68 formatter.print_error(f"Failed to check capacity: {e}") 

69 sys.exit(1) 

70 

71 

72@capacity.command("recommend") 

73@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

74@click.option("--region", "-r", required=True, help="AWS region") 

75@click.option( 

76 "--fault-tolerance", 

77 "-f", 

78 type=click.Choice(["high", "medium", "low"]), 

79 default="medium", 

80 help="Fault tolerance level", 

81) 

82@pass_config 

83def recommend_capacity(config: Any, instance_type: Any, region: Any, fault_tolerance: Any) -> None: 

84 """Get capacity type recommendation for a workload.""" 

85 formatter = get_output_formatter(config) 

86 checker = get_capacity_checker(config) 

87 

88 try: 

89 capacity_type, explanation = checker.recommend_capacity_type( 

90 instance_type, region, fault_tolerance 

91 ) 

92 

93 formatter.print_info(f"Recommended: {capacity_type.upper()}") 

94 formatter.print_info(f"Reason: {explanation}") 

95 

96 except Exception as e: 

97 formatter.print_error(f"Failed to get recommendation: {e}") 

98 sys.exit(1) 

99 

100 

101@capacity.command("spot-prices") 

102@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

103@click.option("--region", "-r", required=True, help="AWS region") 

104@click.option("--days", "-d", default=7, help="Days of history") 

105@pass_config 

106def spot_prices(config: Any, instance_type: Any, region: Any, days: Any) -> None: 

107 """Get spot price history for an instance type.""" 

108 formatter = get_output_formatter(config) 

109 checker = get_capacity_checker(config) 

110 

111 try: 

112 prices = checker.get_spot_price_history(instance_type, region, days) 

113 

114 if not prices: 

115 formatter.print_warning(f"No spot price data for {instance_type} in {region}") 

116 return 

117 

118 formatter.print( 

119 prices, 

120 columns=[ 

121 "availability_zone", 

122 "current_price", 

123 "avg_price_7d", 

124 "min_price_7d", 

125 "max_price_7d", 

126 "price_stability", 

127 ], 

128 ) 

129 

130 except Exception as e: 

131 formatter.print_error(f"Failed to get spot prices: {e}") 

132 sys.exit(1) 

133 

134 

135@capacity.command("instance-info") 

136@click.argument("instance_type") 

137@pass_config 

138def instance_info(config: Any, instance_type: Any) -> None: 

139 """Get information about an instance type.""" 

140 formatter = get_output_formatter(config) 

141 checker = get_capacity_checker(config) 

142 

143 try: 

144 info = checker.get_instance_info(instance_type) 

145 if info: 

146 formatter.print(info) 

147 else: 

148 formatter.print_error(f"Instance type {instance_type} not found") 

149 sys.exit(1) 

150 except Exception as e: 

151 formatter.print_error(f"Failed to get instance info: {e}") 

152 sys.exit(1) 

153 

154 

155@capacity.command("status") 

156@click.option("--region", "-r", help="Specific region to check") 

157@click.option("--all-regions", "-a", is_flag=True, default=True, help="Check all regions (default)") 

158@pass_config 

159def capacity_status(config: Any, region: Any, all_regions: Any) -> None: 

160 """Show comprehensive resource utilization across regions. 

161 

162 Displays pending/running workloads, GPU/CPU utilization, queue depth, 

163 and active job counts for one or all GCO clusters. 

164 

165 Examples: 

166 gco capacity status 

167 gco capacity status --region us-east-1 

168 gco capacity status --all-regions 

169 """ 

170 from ..capacity import get_multi_region_capacity_checker 

171 

172 formatter = get_output_formatter(config) 

173 

174 try: 

175 checker = get_multi_region_capacity_checker(config) 

176 

177 if region: 

178 capacity = checker.get_region_capacity(region) 

179 formatter.print(capacity) 

180 else: 

181 capacities = checker.get_all_regions_capacity() 

182 

183 if not capacities: 

184 formatter.print_warning("No GCO stacks found") 

185 return 

186 

187 # Format as table 

188 print("\n REGION QUEUE RUNNING GPU% CPU% SCORE") 

189 print(" " + "-" * 55) 

190 for c in sorted(capacities, key=lambda x: x.recommendation_score): 

191 print( 

192 f" {c.region:<15} {c.queue_depth:>5} {c.running_jobs:>7} " 

193 f"{c.gpu_utilization:>4.0f}% {c.cpu_utilization:>4.0f}% {c.recommendation_score:>5.0f}" 

194 ) 

195 

196 # Show recommendation 

197 print() 

198 best = min(capacities, key=lambda x: x.recommendation_score) 

199 formatter.print_info(f"Recommended region: {best.region} (lowest score = best)") 

200 

201 except Exception as e: 

202 formatter.print_error(f"Failed to get capacity status: {e}") 

203 sys.exit(1) 

204 

205 

206@capacity.command("recommend-region") 

207@click.option("--gpu", is_flag=True, help="Job requires GPUs") 

208@click.option("--min-gpus", default=0, help="Minimum GPUs required") 

209@click.option( 

210 "--instance-type", "-i", default=None, help="Specific instance type for workload-aware scoring" 

211) 

212@click.option("--gpu-count", default=0, help="Number of GPUs required") 

213@pass_config 

214def recommend_region( 

215 config: Any, gpu: Any, min_gpus: Any, instance_type: Any, gpu_count: Any 

216) -> None: 

217 """Recommend optimal region for job placement. 

218 

219 Analyzes capacity across all deployed EKS regions and recommends 

220 the best region. When --instance-type is provided, uses weighted 

221 multi-signal scoring that factors in spot placement scores, pricing, 

222 queue depth, GPU utilization, and running job counts. 

223 

224 Without --instance-type, uses a simpler composite score based on 

225 queue depth, GPU utilization, and running jobs. 

226 

227 Examples: 

228 gco capacity recommend-region 

229 gco capacity recommend-region --gpu 

230 gco capacity recommend-region -i g5.xlarge 

231 gco capacity recommend-region -i p4d.24xlarge --gpu-count 8 

232 """ 

233 from ..capacity import get_multi_region_capacity_checker 

234 

235 formatter = get_output_formatter(config) 

236 

237 try: 

238 checker = get_multi_region_capacity_checker(config) 

239 recommendation = checker.recommend_region_for_job( 

240 gpu_required=gpu, 

241 min_gpus=min_gpus, 

242 instance_type=instance_type, 

243 gpu_count=gpu_count, 

244 ) 

245 

246 formatter.print_success(f"Recommended region: {recommendation['region']}") 

247 formatter.print_info(f"Reason: {recommendation['reason']}") 

248 

249 if config.verbose: 

250 print("\nAll regions ranked:") 

251 for r in recommendation.get("all_regions", []): 

252 print( 

253 f" {r['region']}: score={r['score']:.4f}, " 

254 f"queue={r['queue_depth']}, gpu={r['gpu_utilization']:.0f}%" 

255 ) 

256 

257 except Exception as e: 

258 formatter.print_error(f"Failed to get recommendation: {e}") 

259 sys.exit(1) 

260 

261 

262@capacity.command("ai-recommend") 

263@click.option("--workload", "-w", help="Description of your workload") 

264@click.option( 

265 "--instance-type", 

266 "-i", 

267 multiple=True, 

268 help="Instance types to consider (can specify multiple)", 

269) 

270@click.option("--region", "-r", multiple=True, help="Regions to consider (can specify multiple)") 

271@click.option("--gpu", is_flag=True, help="Workload requires GPUs") 

272@click.option("--min-gpus", default=0, help="Minimum GPUs required") 

273@click.option("--min-memory-gb", default=0, help="Minimum memory in GB") 

274@click.option( 

275 "--fault-tolerance", 

276 "-f", 

277 type=click.Choice(["high", "medium", "low"]), 

278 default="medium", 

279 help="Fault tolerance level", 

280) 

281@click.option("--max-cost", type=float, help="Maximum cost per hour in USD") 

282@click.option( 

283 "--model", 

284 "-m", 

285 default=None, 

286 help="Bedrock model ID to use (default: cdk.json context.bedrock.default_model_id).", 

287) 

288@click.option("--raw", is_flag=True, help="Show raw AI response") 

289@pass_config 

290def ai_recommend( 

291 config: Any, 

292 workload: Any, 

293 instance_type: Any, 

294 region: Any, 

295 gpu: Any, 

296 min_gpus: Any, 

297 min_memory_gb: Any, 

298 fault_tolerance: Any, 

299 max_cost: Any, 

300 model: Any, 

301 raw: Any, 

302) -> None: 

303 """Get AI-powered capacity recommendation using Amazon Bedrock. 

304 

305 This command gathers comprehensive capacity data including: 

306 - Spot placement scores, pricing, and 7-day per-AZ price trends across regions 

307 - On-demand availability and pricing 

308 - Capacity Reservations (ODCRs), Capacity Block offerings, and 26-week 

309 block-availability trends 

310 - Current cluster utilization (queue depth, GPU/CPU usage) 

311 - Running and pending job counts 

312 - The algorithmic multi-signal region ranking as advisory context 

313 

314 Without --instance-type, one representative type per current GPU 

315 generation is scanned (T4, L4, A10G, L40S, RTX PRO 4500/6000 Blackwell, 

316 A100, H100, H200, B200, B300). 

317 

318 The data is analyzed by an LLM to provide intelligent recommendations 

319 for where to place your workload. 

320 

321 ⚠️ DISCLAIMER: Recommendations are AI-generated and should be validated 

322 before making production decisions. Capacity availability and pricing 

323 can change rapidly. 

324 

325 REQUIREMENTS: 

326 - AWS credentials with bedrock:InvokeModel permission 

327 - The specified Bedrock model must be enabled in your account 

328 - Default model: cdk.json context.bedrock.default_model_id 

329 

330 Examples: 

331 gco capacity ai-recommend --workload "Training a large language model" 

332 

333 gco capacity ai-recommend -w "Inference workload" --gpu --min-gpus 4 

334 

335 gco capacity ai-recommend -i g5.xlarge -i g5.2xlarge -r us-east-1 -r us-west-2 

336 

337 gco capacity ai-recommend --fault-tolerance high --max-cost 5.00 

338 """ 

339 from ..capacity import get_bedrock_capacity_advisor 

340 

341 formatter = get_output_formatter(config) 

342 

343 # Print disclaimer 

344 print() 

345 print(" " + "=" * 70) 

346 print(" ⚠️ AI-POWERED RECOMMENDATION DISCLAIMER") 

347 print(" " + "-" * 70) 

348 print(" This recommendation is generated by an AI model and should be") 

349 print(" validated before making production decisions.") 

350 print(" ") 

351 print(" • Capacity availability can change rapidly") 

352 print(" • Spot instances may be interrupted at any time") 

353 print(" • Pricing data may not reflect real-time prices") 

354 print(" • AI recommendations are not guaranteed to be optimal") 

355 print(" " + "=" * 70) 

356 print() 

357 

358 try: 

359 formatter.print_info("Gathering capacity data across regions...") 

360 

361 advisor = get_bedrock_capacity_advisor(config, model_id=model) 

362 

363 # Build requirements dict 

364 requirements = { 

365 "gpu_required": gpu, 

366 "min_gpus": min_gpus if min_gpus > 0 else None, 

367 "min_memory_gb": min_memory_gb if min_memory_gb > 0 else None, 

368 "fault_tolerance": fault_tolerance, 

369 "max_cost_per_hour": max_cost, 

370 } 

371 # Remove None values 

372 requirements = {k: v for k, v in requirements.items() if v is not None} 

373 

374 formatter.print_info(f"Analyzing with {advisor.model_id}...") 

375 

376 recommendation = advisor.get_recommendation( 

377 workload_description=workload, 

378 instance_types=list(instance_type) if instance_type else None, 

379 regions=list(region) if region else None, 

380 requirements=requirements if requirements else None, 

381 ) 

382 

383 # Display recommendation 

384 print() 

385 print(" " + "=" * 70) 

386 print(" 🤖 AI RECOMMENDATION") 

387 print(" " + "=" * 70) 

388 print() 

389 print(f" Region: {recommendation.recommended_region}") 

390 print(f" Instance Type: {recommendation.recommended_instance_type}") 

391 print(f" Capacity Type: {recommendation.recommended_capacity_type.upper()}") 

392 print(f" Confidence: {recommendation.confidence.upper()}") 

393 if recommendation.cost_estimate: 

394 print(f" Est. Cost: {recommendation.cost_estimate}") 

395 print() 

396 print(" REASONING:") 

397 print(" " + "-" * 68) 

398 # Word wrap the reasoning 

399 reasoning_lines = recommendation.reasoning.split(". ") 

400 for line in reasoning_lines: 

401 if line.strip(): 

402 print(f" {line.strip()}.") 

403 print() 

404 

405 # Show alternatives 

406 if recommendation.alternative_options: 

407 print(" ALTERNATIVE OPTIONS:") 

408 print(" " + "-" * 68) 

409 for i, alt in enumerate(recommendation.alternative_options[:3], 1): 

410 print( 

411 f" {i}. {alt.get('region', 'N/A')} / " 

412 f"{alt.get('instance_type', 'N/A')} / " 

413 f"{alt.get('capacity_type', 'N/A').upper()}" 

414 ) 

415 if alt.get("reason"): 

416 print(f" {alt['reason']}") 

417 print() 

418 

419 # Show warnings 

420 if recommendation.warnings: 

421 print(" ⚠️ WARNINGS:") 

422 print(" " + "-" * 68) 

423 for warning in recommendation.warnings: 

424 print(f"{warning}") 

425 print() 

426 

427 # Show raw response if requested 

428 if raw: 

429 print(" RAW AI RESPONSE:") 

430 print(" " + "-" * 68) 

431 print(recommendation.raw_response) 

432 print() 

433 

434 print(" " + "=" * 70) 

435 print() 

436 

437 except Exception as e: 

438 if is_bedrock_ftu_form_error(e): 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true

439 formatter.print_error(BEDROCK_FTU_REMEDIATION) 

440 sys.exit(1) 

441 # Advisor errors are already fully worded (and may carry their own 

442 # remediation); re-prefixing here used to print the same phrase twice. 

443 formatter.print_error(str(e)) 

444 sys.exit(1) 

445 

446 

447@capacity.command("reservations") 

448@click.option("--instance-type", "-i", help="Filter by instance type") 

449@click.option("--region", "-r", help="Specific region (default: all deployed regions)") 

450@pass_config 

451def list_reservations(config: Any, instance_type: Any, region: Any) -> None: 

452 """List On-Demand Capacity Reservations (ODCRs) across regions. 

453 

454 Shows all active capacity reservations with utilization details. 

455 

456 Examples: 

457 gco capacity reservations 

458 gco capacity reservations -i p5.48xlarge 

459 gco capacity reservations -r us-east-1 

460 """ 

461 formatter = get_output_formatter(config) 

462 checker = get_capacity_checker(config) 

463 

464 try: 

465 if region: 

466 reservations = checker.list_capacity_reservations(region, instance_type=instance_type) 

467 result = { 

468 "regions_checked": [region], 

469 "total_reservations": len(reservations), 

470 "total_reserved_instances": sum(r["total_instances"] for r in reservations), 

471 "total_available_instances": sum(r["available_instances"] for r in reservations), 

472 "reservations": reservations, 

473 } 

474 else: 

475 result = checker.list_all_reservations(instance_type=instance_type) 

476 

477 if config.output_format != "table": 

478 formatter.print(result) 

479 return 

480 

481 reservations = result["reservations"] 

482 if not reservations: 

483 formatter.print_info("No active capacity reservations found") 

484 return 

485 

486 print(f"\n Capacity Reservations ({len(reservations)} found)") 

487 print(" " + "-" * 90) 

488 print( 

489 f" {'INSTANCE TYPE':<18} {'REGION':<15} {'AZ':<18} " 

490 f"{'TOTAL':>5} {'AVAIL':>5} {'USED%':>6} {'MATCH CRITERIA'}" 

491 ) 

492 print(" " + "-" * 90) 

493 for r in reservations: 

494 print( 

495 f" {r['instance_type']:<18} {r['region']:<15} " 

496 f"{r['availability_zone']:<18} {r['total_instances']:>5} " 

497 f"{r['available_instances']:>5} {r['utilization_pct']:>5.1f}% " 

498 f"{r.get('instance_match_criteria', 'open')}" 

499 ) 

500 

501 print() 

502 print( 

503 f" Total: {result['total_reserved_instances']} reserved, " 

504 f"{result['total_available_instances']} available" 

505 ) 

506 print() 

507 

508 except Exception as e: 

509 formatter.print_error(f"Failed to list reservations: {e}") 

510 sys.exit(1) 

511 

512 

513@capacity.command("reservation-check") 

514@click.option("--instance-type", "-i", required=True, help="Instance type to check") 

515@click.option( 

516 "--region", 

517 "-r", 

518 "regions", 

519 multiple=True, 

520 help="Region(s) to check; repeatable (default: all deployed regions)", 

521) 

522@click.option("--count", "-c", default=1, help="Minimum instances needed") 

523@click.option( 

524 "--include-blocks/--no-blocks", 

525 default=True, 

526 help="Include Capacity Block offerings (default: yes)", 

527) 

528@click.option( 

529 "--block-duration", 

530 default=24, 

531 type=int, 

532 help="Capacity Block duration in hours (default: 24)", 

533) 

534@click.option( 

535 "--block-duration-days", 

536 default=None, 

537 type=int, 

538 help="Capacity Block duration in days (overrides --block-duration)", 

539) 

540@click.option( 

541 "--earliest-start", 

542 default=None, 

543 help="Earliest block start date (YYYY-MM-DD or ISO datetime)", 

544) 

545@click.option( 

546 "--latest-start", 

547 default=None, 

548 help="Latest block start date (YYYY-MM-DD or ISO datetime)", 

549) 

550@pass_config 

551def reservation_check( 

552 config: Any, 

553 instance_type: Any, 

554 regions: Any, 

555 count: Any, 

556 include_blocks: Any, 

557 block_duration: Any, 

558 block_duration_days: Any, 

559 earliest_start: Any, 

560 latest_start: Any, 

561) -> None: 

562 """Check reservation availability and Capacity Block offerings. 

563 

564 Checks both existing ODCRs and purchasable Capacity Blocks for ML 

565 workloads. Capacity Blocks provide guaranteed GPU capacity for a 

566 fixed duration at a known price. Pass --region more than once to check 

567 several regions in parallel, and use --earliest-start/--latest-start to 

568 bound when the block may begin. For a full duration-range sweep across 

569 many regions, use 'gco capacity find-blocks'. 

570 

571 Examples: 

572 gco capacity reservation-check -i p5.48xlarge 

573 gco capacity reservation-check -i p4d.24xlarge -c 2 --block-duration 48 

574 gco capacity reservation-check -i g5.48xlarge -r us-east-1 --no-blocks 

575 gco capacity reservation-check -i p5.48xlarge -r us-east-1 -r us-west-2 \\ 

576 --block-duration-days 14 --earliest-start 2026-07-01 

577 """ 

578 formatter = get_output_formatter(config) 

579 checker = get_capacity_checker(config) 

580 

581 try: 

582 formatter.print_info( 

583 f"Checking reservations for {instance_type} " 

584 f"(min {count} instance{'s' if count > 1 else ''})..." 

585 ) 

586 

587 result = checker.check_reservation_availability( 

588 instance_type=instance_type, 

589 regions=list(regions) or None, 

590 min_count=count, 

591 include_capacity_blocks=include_blocks, 

592 block_duration_hours=block_duration, 

593 block_duration_days=block_duration_days, 

594 earliest_start=earliest_start, 

595 latest_start=latest_start, 

596 ) 

597 

598 if config.output_format != "table": 598 ↛ 599line 598 didn't jump to line 599 because the condition on line 598 was never true

599 formatter.print(result) 

600 return 

601 

602 # ODCR section 

603 odcr = result["odcr"] 

604 print(f"\n On-Demand Capacity Reservations for {instance_type}") 

605 print(" " + "-" * 60) 

606 if odcr["reservations"]: 

607 for r in odcr["reservations"]: 

608 print( 

609 f"{r['availability_zone']}: " 

610 f"{r['available_instances']}/{r['total_instances']} available " 

611 f"({r['reservation_id']})" 

612 ) 

613 print( 

614 f"\n Total: {odcr['total_available_instances']} available " 

615 f"of {odcr['total_reserved_instances']} reserved" 

616 ) 

617 else: 

618 print(" No active ODCRs found for this instance type") 

619 

620 # Capacity Blocks section 

621 if include_blocks: 

622 block_section = result["capacity_blocks"] 

623 duration = block_section.get("duration_hours", block_duration) 

624 print(f"\n Capacity Block Offerings ({duration}h)") 

625 print(" " + "-" * 60) 

626 if block_section["offerings"]: 

627 for b in block_section["offerings"]: 

628 gpu_hr = b.get("price_per_gpu_hour") 

629 gpu_hr_str = f" (${gpu_hr}/GPU-hr)" if gpu_hr is not None else "" 

630 start = (b.get("start_date") or "")[:16] 

631 print( 

632 f"{b['availability_zone']}: " 

633 f"{b['instance_count']}x {b['duration_hours']}h " 

634 f"starting {start} — ${b['upfront_fee']}{gpu_hr_str}" 

635 ) 

636 else: 

637 print(" No Capacity Block offerings available") 

638 

639 # Recommendation 

640 print() 

641 print(f" 💡 {result['recommendation']}") 

642 print() 

643 

644 except Exception as e: 

645 formatter.print_error(f"Failed to check reservations: {e}") 

646 sys.exit(1) 

647 

648 

649def _print_find_blocks_report(result: dict[str, Any]) -> None: 

650 """Render a consolidated find-blocks report as a readable table block.""" 

651 itype = result["instance_type"] 

652 if result.get("requested_instance_type") and result["requested_instance_type"] != itype: 652 ↛ 655line 652 didn't jump to line 655 because the condition on line 652 was always true

653 print(f"\n Capacity Block search for {result['requested_instance_type']} -> {itype}") 

654 else: 

655 print(f"\n Capacity Block search for {itype}") 

656 print(" " + "-" * 72) 

657 

658 window = result.get("date_window", {}) 

659 earliest = (window.get("earliest_start") or "any")[:16] 

660 latest = (window.get("latest_start") or "any")[:16] 

661 days = result.get("durations_probed_days") or [] 

662 if days: 662 ↛ 665line 662 didn't jump to line 665 because the condition on line 662 was always true

663 span = f"{min(days):g}-{max(days):g}d" if len(days) > 1 else f"{days[0]:g}d" 

664 else: 

665 span = "n/a" 

666 print(f" Regions: {', '.join(result.get('regions_checked', []))}") 

667 print(f" Durations probed: {span} Start window: {earliest} .. {latest}") 

668 

669 if not result.get("valid_instance_type", True): 

670 print() 

671 print(f"{result.get('recommendation', 'Invalid instance type.')}") 

672 print() 

673 return 

674 

675 offerings = result.get("offerings", []) 

676 if not offerings: 

677 print() 

678 print(f" {result.get('recommendation', 'No offerings found.')}") 

679 print() 

680 return 

681 

682 print() 

683 print(f" {'REGION':<13} {'AZ':<17} {'START':<17} {'DUR':>6} {'UPFRONT':>11} {'$/GPU-hr':>10}") 

684 print(" " + "-" * 72) 

685 for b in offerings: 

686 start = (b.get("start_date") or "")[:16] 

687 dur = f"{b.get('duration_days') or '?'}d" 

688 fee = b.get("upfront_fee_usd") 

689 fee_str = f"${fee:,.0f}" if isinstance(fee, int | float) else "?" 

690 gpu_hr = b.get("price_per_gpu_hour") 

691 gpu_hr_str = f"${gpu_hr:,.2f}" if isinstance(gpu_hr, int | float) else "-" 

692 print( 

693 f" {str(b.get('region') or ''):<13} {str(b.get('availability_zone') or ''):<17} " 

694 f"{start:<17} {dur:>6} {fee_str:>11} {gpu_hr_str:>10}" 

695 ) 

696 print() 

697 print(f" 💡 {result['recommendation']}") 

698 print() 

699 

700 

701@capacity.command("find-blocks") 

702@click.option( 

703 "--instance-type", "-i", required=True, help="GPU instance type or alias (e.g. p6-b200)" 

704) 

705@click.option( 

706 "--region", 

707 "-r", 

708 "regions", 

709 multiple=True, 

710 help="Region(s) to search; repeatable (default: all deployed regions)", 

711) 

712@click.option("--count", "-c", default=1, help="Instances per block") 

713@click.option("--duration-days", default=None, type=int, help="Single target duration in days") 

714@click.option("--duration-hours", default=None, type=int, help="Single target duration in hours") 

715@click.option( 

716 "--min-duration-days", default=None, type=int, help="Minimum duration (days) for a range search" 

717) 

718@click.option( 

719 "--max-duration-days", default=None, type=int, help="Maximum duration (days) for a range search" 

720) 

721@click.option("--min-duration-hours", default=None, type=int, help="Minimum duration (hours)") 

722@click.option("--max-duration-hours", default=None, type=int, help="Maximum duration (hours)") 

723@click.option( 

724 "--earliest-start", default=None, help="Earliest block start (YYYY-MM-DD or ISO datetime)" 

725) 

726@click.option( 

727 "--latest-start", default=None, help="Latest block start (YYYY-MM-DD or ISO datetime)" 

728) 

729@click.option( 

730 "--find-longest", 

731 is_flag=True, 

732 help="Sweep the duration ladder and surface the longest available block", 

733) 

734@pass_config 

735def find_blocks( 

736 config: Any, 

737 instance_type: Any, 

738 regions: Any, 

739 count: Any, 

740 duration_days: Any, 

741 duration_hours: Any, 

742 min_duration_days: Any, 

743 max_duration_days: Any, 

744 min_duration_hours: Any, 

745 max_duration_hours: Any, 

746 earliest_start: Any, 

747 latest_start: Any, 

748 find_longest: Any, 

749) -> None: 

750 """Find Capacity Blocks across regions, durations, and a start-date window. 

751 

752 One command sweeps every requested region and every valid Capacity Block 

753 duration in the range, in parallel, then returns a single consolidated, 

754 de-duplicated, ranked report with per-hour and per-GPU-hour pricing. 

755 

756 AWS allows Capacity Block durations in 1-day increments up to 14 days, then 

757 7-day increments up to 182 days; a duration range is expanded to those 

758 discrete values automatically. Friendly names are normalized (p6-b200 -> 

759 p6-b200.48xlarge, p6-b300 -> p6-b300.48xlarge); the Grace-Blackwell GB200/ 

760 GB300 UltraServer families (P6e-GB200/P6e-GB300) are flagged as not standalone. 

761 

762 Examples: 

763 gco capacity find-blocks -i p6-b200.48xlarge \\ 

764 -r us-east-1 -r us-east-2 -r us-west-2 -r eu-west-1 \\ 

765 --min-duration-days 1 --max-duration-days 63 \\ 

766 --earliest-start 2026-07-01 --latest-start 2026-07-10 

767 gco capacity find-blocks -i p5.48xlarge -r us-east-1 --duration-days 14 

768 gco capacity find-blocks -i p5.48xlarge -r us-east-1 --find-longest 

769 """ 

770 formatter = get_output_formatter(config) 

771 checker = get_capacity_checker(config) 

772 

773 try: 

774 result = checker.find_capacity_blocks( 

775 instance_type, 

776 regions=list(regions) or None, 

777 instance_count=count, 

778 duration_hours=duration_hours, 

779 duration_days=duration_days, 

780 min_duration_hours=min_duration_hours, 

781 min_duration_days=min_duration_days, 

782 max_duration_hours=max_duration_hours, 

783 max_duration_days=max_duration_days, 

784 earliest_start=earliest_start, 

785 latest_start=latest_start, 

786 find_longest=find_longest, 

787 ) 

788 

789 if config.output_format != "table": 

790 formatter.print(result) 

791 return 

792 

793 _print_find_blocks_report(result) 

794 

795 except Exception as e: 

796 formatter.print_error(f"Failed to find capacity blocks: {e}") 

797 sys.exit(1) 

798 

799 

800@capacity.command("reserve") 

801@click.option( 

802 "--offering-id", 

803 "-o", 

804 required=True, 

805 help="Capacity Block offering ID (cb-xxx) from reservation-check", 

806) 

807@click.option("--region", "-r", required=True, help="AWS region where the offering exists") 

808@click.option( 

809 "--dry-run", 

810 is_flag=True, 

811 help="Validate the offering without purchasing (no cost incurred)", 

812) 

813@pass_config 

814def reserve_capacity(config: Any, offering_id: Any, region: Any, dry_run: Any) -> None: 

815 """Purchase a Capacity Block offering by its ID. 

816 

817 Use 'gco capacity reservation-check' first to find available offerings 

818 and their IDs, then purchase with this command. 

819 

820 ⚠️ WARNING: This command purchases capacity and incurs charges. 

821 Use --dry-run to validate first. 

822 

823 Examples: 

824 # First, find offerings: 

825 gco capacity reservation-check -i p4d.24xlarge -r us-east-1 

826 

827 # Validate without purchasing: 

828 gco capacity reserve -o cb-0123456789abcdef0 -r us-east-1 --dry-run 

829 

830 # Purchase: 

831 gco capacity reserve -o cb-0123456789abcdef0 -r us-east-1 

832 """ 

833 formatter = get_output_formatter(config) 

834 checker = get_capacity_checker(config) 

835 

836 try: 

837 if dry_run: 

838 formatter.print_info(f"Dry run: validating offering {offering_id} in {region}...") 

839 else: 

840 formatter.print_info(f"Purchasing Capacity Block {offering_id} in {region}...") 

841 

842 result = checker.purchase_capacity_block( 

843 offering_id=offering_id, 

844 region=region, 

845 dry_run=dry_run, 

846 ) 

847 

848 if config.output_format != "table": 848 ↛ 849line 848 didn't jump to line 849 because the condition on line 848 was never true

849 formatter.print(result) 

850 return 

851 

852 if result["success"]: 

853 if dry_run: 

854 print() 

855 print(f" ✓ Dry run passed — offering {offering_id} is valid and purchasable") 

856 print(f" Region: {region}") 

857 print() 

858 print(" To purchase, run without --dry-run:") 

859 print(f" gco capacity reserve -o {offering_id} -r {region}") 

860 print() 

861 else: 

862 print() 

863 print(" ✓ Capacity Block purchased successfully") 

864 print(f" Reservation ID: {result['reservation_id']}") 

865 print(f" Instance Type: {result['instance_type']}") 

866 print(f" AZ: {result['availability_zone']}") 

867 print(f" Instances: {result['total_instances']}") 

868 print(f" Start: {result.get('start_date', 'N/A')}") 

869 print(f" End: {result.get('end_date', 'N/A')}") 

870 print() 

871 print(" To create a NodePool for this reservation:") 

872 print( 

873 f" gco nodepools create-odcr -n my-pool -r {region} " 

874 f"-c {result['reservation_id']} -i {result['instance_type']}" 

875 ) 

876 print() 

877 else: 

878 formatter.print_error( 

879 f"Failed: {result.get('error_code', 'Unknown')}: {result.get('error', '')}" 

880 ) 

881 sys.exit(1) 

882 

883 except Exception as e: 

884 formatter.print_error(f"Failed to reserve capacity: {e}") 

885 sys.exit(1) 

886 

887 

888def _print_find_reservations_report(result: dict[str, Any]) -> None: 

889 """Render a consolidated find-reservations report as a readable table block.""" 

890 itype = result.get("instance_type") or "any instance type" 

891 req = result.get("requested_instance_type") 

892 if req and req != itype: 892 ↛ 893line 892 didn't jump to line 893 because the condition on line 892 was never true

893 print(f"\n ODCR search for {req} -> {itype}") 

894 else: 

895 print(f"\n ODCR search for {itype}") 

896 print(" " + "-" * 78) 

897 print(f" Regions: {', '.join(result.get('regions_checked', []))}") 

898 

899 if req and not result.get("valid_instance_type", True): 899 ↛ 900line 899 didn't jump to line 900 because the condition on line 899 was never true

900 print() 

901 print(f"{result.get('recommendation', 'Invalid instance type.')}") 

902 print() 

903 return 

904 

905 reservations = result.get("reservations", []) 

906 if not reservations: 

907 print() 

908 print(f" {result.get('recommendation', 'No reservations found.')}") 

909 print() 

910 return 

911 

912 print() 

913 print( 

914 f" {'INSTANCE TYPE':<18} {'REGION':<13} {'AZ':<17} " 

915 f"{'AVAIL':>6} {'TOTAL':>6} {'$/GPU-hr':>10}" 

916 ) 

917 print(" " + "-" * 78) 

918 for r in reservations: 

919 gpu_hr = r.get("price_per_gpu_hour") 

920 gpu_hr_str = f"${gpu_hr:,.2f}" if isinstance(gpu_hr, int | float) else "-" 

921 print( 

922 f" {str(r.get('instance_type') or ''):<18} {str(r.get('region') or ''):<13} " 

923 f"{str(r.get('availability_zone') or ''):<17} " 

924 f"{r.get('available_instances', 0):>6} {r.get('total_instances', 0):>6} " 

925 f"{gpu_hr_str:>10}" 

926 ) 

927 print() 

928 print(f" 💡 {result['recommendation']}") 

929 print() 

930 

931 

932@capacity.command("find-reservations") 

933@click.option( 

934 "--instance-type", 

935 "-i", 

936 default=None, 

937 help="Instance type or alias to filter by (e.g. p6-b200); omit for all types", 

938) 

939@click.option( 

940 "--region", 

941 "-r", 

942 "regions", 

943 multiple=True, 

944 help="Region(s) to search; repeatable (default: all deployed regions)", 

945) 

946@click.option( 

947 "--count", 

948 "-c", 

949 default=1, 

950 help="Minimum available instances to consider the search satisfied", 

951) 

952@click.option( 

953 "--state", 

954 default="active", 

955 help="Reservation state filter (default: active; use 'all' for any state)", 

956) 

957@click.option( 

958 "--pricing/--no-pricing", 

959 default=True, 

960 help="Enrich reservations with On-Demand pricing (default: yes)", 

961) 

962@pass_config 

963def find_reservations( 

964 config: Any, 

965 instance_type: Any, 

966 regions: Any, 

967 count: Any, 

968 state: Any, 

969 pricing: Any, 

970) -> None: 

971 """Find existing ODCRs across regions in one parallel, ranked report. 

972 

973 The ODCR counterpart to 'gco capacity find-blocks': it searches every 

974 requested region in parallel, normalizes friendly instance-type aliases 

975 (p6-b200 -> p6-b200.48xlarge), enriches each reservation with On-Demand 

976 pricing, and ranks them most-available-first (then cheapest per-GPU-hour). 

977 

978 Examples: 

979 gco capacity find-reservations -i p5.48xlarge 

980 gco capacity find-reservations -i p6-b200 -r us-east-1 -r us-west-2 

981 gco capacity find-reservations --no-pricing 

982 """ 

983 formatter = get_output_formatter(config) 

984 checker = get_capacity_checker(config) 

985 

986 try: 

987 result = checker.find_capacity_reservations( 

988 instance_type=instance_type, 

989 regions=list(regions) or None, 

990 min_count=count, 

991 state=None if str(state).lower() == "all" else state, 

992 include_pricing=pricing, 

993 ) 

994 

995 if config.output_format != "table": 995 ↛ 996line 995 didn't jump to line 996 because the condition on line 995 was never true

996 formatter.print(result) 

997 return 

998 

999 _print_find_reservations_report(result) 

1000 

1001 except Exception as e: 

1002 formatter.print_error(f"Failed to find reservations: {e}") 

1003 sys.exit(1) 

1004 

1005 

1006@capacity.command("create-reservation") 

1007@click.option("--instance-type", "-i", required=True, help="EC2 instance type or alias") 

1008@click.option("--region", "-r", required=True, help="AWS region") 

1009@click.option( 

1010 "--availability-zone", "-z", required=True, help="Target Availability Zone (e.g. us-east-1a)" 

1011) 

1012@click.option("--count", "-c", default=1, help="Number of instances to reserve") 

1013@click.option("--platform", default="Linux/UNIX", help="Instance platform/OS (default: Linux/UNIX)") 

1014@click.option( 

1015 "--tenancy", 

1016 type=click.Choice(["default", "dedicated"]), 

1017 default="default", 

1018 help="Reservation tenancy (default: default)", 

1019) 

1020@click.option( 

1021 "--match-criteria", 

1022 type=click.Choice(["open", "targeted"]), 

1023 default="open", 

1024 help="Instance match criteria (default: open)", 

1025) 

1026@click.option( 

1027 "--end-date", 

1028 default=None, 

1029 help="Optional end date (YYYY-MM-DD or ISO datetime); omit for an unlimited reservation", 

1030) 

1031@click.option("--ebs-optimized", is_flag=True, help="Reserve EBS-optimized capacity") 

1032@click.option( 

1033 "--dry-run", 

1034 is_flag=True, 

1035 help="Validate the request without creating (no cost incurred)", 

1036) 

1037@pass_config 

1038def create_reservation( 

1039 config: Any, 

1040 instance_type: Any, 

1041 region: Any, 

1042 availability_zone: Any, 

1043 count: Any, 

1044 platform: Any, 

1045 tenancy: Any, 

1046 match_criteria: Any, 

1047 end_date: Any, 

1048 ebs_optimized: Any, 

1049 dry_run: Any, 

1050) -> None: 

1051 """Create a new On-Demand Capacity Reservation (ODCR). 

1052 

1053 The ODCR counterpart to 'gco capacity reserve'. Reserves On-Demand capacity 

1054 for an instance type in a specific AZ. 

1055 

1056 ⚠️ WARNING: creating a reservation incurs On-Demand charges for the reserved 

1057 capacity whether or not it is used, until the reservation is cancelled. 

1058 Use --dry-run to validate first. 

1059 

1060 Examples: 

1061 gco capacity create-reservation -i p5.48xlarge -r us-east-1 -z us-east-1a -c 2 --dry-run 

1062 gco capacity create-reservation -i p6-b200 -r us-east-1 -z us-east-1a -c 1 

1063 gco capacity create-reservation -i p4d.24xlarge -r us-west-2 -z us-west-2b \\ 

1064 --end-date 2026-08-01 

1065 """ 

1066 formatter = get_output_formatter(config) 

1067 checker = get_capacity_checker(config) 

1068 

1069 try: 

1070 if dry_run: 

1071 formatter.print_info( 

1072 f"Dry run: validating reservation for {count}x {instance_type} " 

1073 f"in {availability_zone}..." 

1074 ) 

1075 else: 

1076 formatter.print_info( 

1077 f"Creating reservation for {count}x {instance_type} in {availability_zone}..." 

1078 ) 

1079 

1080 result = checker.create_capacity_reservation( 

1081 instance_type=instance_type, 

1082 region=region, 

1083 availability_zone=availability_zone, 

1084 instance_count=count, 

1085 instance_platform=platform, 

1086 tenancy=tenancy, 

1087 instance_match_criteria=match_criteria, 

1088 end_date=end_date, 

1089 ebs_optimized=ebs_optimized, 

1090 dry_run=dry_run, 

1091 ) 

1092 

1093 if config.output_format != "table": 1093 ↛ 1094line 1093 didn't jump to line 1094 because the condition on line 1093 was never true

1094 formatter.print(result) 

1095 return 

1096 

1097 if result["success"]: 

1098 if dry_run: 

1099 print() 

1100 print(" ✓ Dry run passed — reservation parameters are valid") 

1101 print(f" Instance Type: {result.get('instance_type')}") 

1102 print(f" AZ: {result.get('availability_zone')}") 

1103 print(f" Instances: {result.get('instance_count')}") 

1104 print() 

1105 print(" To create, run without --dry-run:") 

1106 print( 

1107 f" gco capacity create-reservation -i {result.get('instance_type')} " 

1108 f"-r {region} -z {availability_zone} -c {count}" 

1109 ) 

1110 print() 

1111 else: 

1112 print() 

1113 print(" ✓ Capacity Reservation created successfully") 

1114 print(f" Reservation ID: {result['reservation_id']}") 

1115 print(f" Instance Type: {result['instance_type']}") 

1116 print(f" AZ: {result['availability_zone']}") 

1117 print(f" Instances: {result['total_instances']}") 

1118 print(f" State: {result.get('state', 'N/A')}") 

1119 print(f" End: {result.get('end_date') or 'unlimited'}") 

1120 print() 

1121 print(" To create a NodePool for this reservation:") 

1122 print( 

1123 f" gco nodepools create-odcr -n my-pool -r {region} " 

1124 f"-c {result['reservation_id']} -i {result['instance_type']}" 

1125 ) 

1126 print() 

1127 else: 

1128 formatter.print_error( 

1129 f"Failed: {result.get('error_code', 'Unknown')}: {result.get('error', '')}" 

1130 ) 

1131 sys.exit(1) 

1132 

1133 except Exception as e: 

1134 formatter.print_error(f"Failed to create reservation: {e}") 

1135 sys.exit(1) 

1136 

1137 

1138@capacity.command("cancel-reservation") 

1139@click.option( 

1140 "--reservation-id", "-o", required=True, help="Capacity Reservation ID (cr-xxx) to cancel" 

1141) 

1142@click.option("--region", "-r", required=True, help="AWS region where the reservation exists") 

1143@click.option( 

1144 "--dry-run", is_flag=True, help="Validate the cancellation without cancelling (no change)" 

1145) 

1146@click.option("--yes", "-y", is_flag=True, help="Skip confirmation") 

1147@pass_config 

1148def cancel_reservation( 

1149 config: Any, reservation_id: Any, region: Any, dry_run: Any, yes: Any 

1150) -> None: 

1151 """Cancel an On-Demand Capacity Reservation, releasing its capacity. 

1152 

1153 Stops On-Demand charges for the reserved capacity. Only ODCRs can be 

1154 cancelled; a Capacity Block runs for its fixed term. Instances already 

1155 running against the reservation are not terminated — they revert to normal 

1156 On-Demand billing. 

1157 

1158 Examples: 

1159 gco capacity cancel-reservation -o cr-0123456789abcdef0 -r us-east-1 --dry-run 

1160 gco capacity cancel-reservation -o cr-0123456789abcdef0 -r us-east-1 -y 

1161 """ 

1162 formatter = get_output_formatter(config) 

1163 checker = get_capacity_checker(config) 

1164 

1165 if not dry_run and not yes: 

1166 click.confirm(f"Cancel capacity reservation '{reservation_id}' in {region}?", abort=True) 

1167 

1168 try: 

1169 if dry_run: 

1170 formatter.print_info(f"Dry run: validating cancellation of {reservation_id}...") 

1171 else: 

1172 formatter.print_info(f"Cancelling capacity reservation {reservation_id}...") 

1173 

1174 result = checker.cancel_capacity_reservation( 

1175 reservation_id=reservation_id, 

1176 region=region, 

1177 dry_run=dry_run, 

1178 ) 

1179 

1180 if config.output_format != "table": 1180 ↛ 1181line 1180 didn't jump to line 1181 because the condition on line 1180 was never true

1181 formatter.print(result) 

1182 return 

1183 

1184 if result["success"]: 

1185 print() 

1186 if dry_run: 

1187 print(f" ✓ Dry run passed — {reservation_id} can be cancelled") 

1188 else: 

1189 print(f"{result.get('message', 'Reservation cancelled.')}") 

1190 print() 

1191 else: 

1192 formatter.print_error( 

1193 f"Failed: {result.get('error_code', 'Unknown')}: {result.get('error', '')}" 

1194 ) 

1195 sys.exit(1) 

1196 

1197 except Exception as e: 

1198 formatter.print_error(f"Failed to cancel reservation: {e}") 

1199 sys.exit(1) 

1200 

1201 

1202_HISTORY_DISABLED_HINT = ( 

1203 "The historical capacity surface is not enabled. It is an optional add-on to " 

1204 "the global stack: set historical.enabled to true in cdk.json and run " 

1205 "'gco stacks deploy gco-global'. See lambda/capacity-poller/README.md." 

1206) 

1207 

1208 

1209def _history_disabled(exc: Exception) -> bool: 

1210 """True if exc is a 'table does not exist' error (feature not deployed).""" 

1211 return ( 

1212 isinstance(exc, ClientError) 

1213 and exc.response.get("Error", {}).get("Code") == "ResourceNotFoundException" 

1214 ) 

1215 

1216 

1217def _print_historical_enrichment(formatter: Any, instance_type: str, region: str) -> None: 

1218 """Append a historical capacity summary to ``gco capacity check`` output.""" 

1219 from ..capacity.history import get_capacity_history_store 

1220 

1221 try: 

1222 stats = get_capacity_history_store().get_statistics(instance_type, region) 

1223 except Exception as e: # supplementary to check; never fail the command 

1224 if _history_disabled(e): 1224 ↛ 1227line 1224 didn't jump to line 1227 because the condition on line 1224 was always true

1225 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1226 else: 

1227 formatter.print_warning(f"Historical enrichment unavailable: {e}") 

1228 return 

1229 if stats["sample_count"] == 0: 1229 ↛ 1230line 1229 didn't jump to line 1230 because the condition on line 1229 was never true

1230 formatter.print_warning( 

1231 f"No historical samples for {instance_type} in {region} yet " 

1232 "(the poller records one about every 15 minutes)." 

1233 ) 

1234 return 

1235 formatter.print_info(f"Historical context (last 7 days, {stats['sample_count']} samples):") 

1236 spot_stats = stats["metrics"].get("spot_score") 

1237 if spot_stats: 1237 ↛ 1243line 1237 didn't jump to line 1243 because the condition on line 1237 was always true

1238 print( 

1239 f" spot_score p25/p50/p75: " 

1240 f"{spot_stats['p25']}/{spot_stats['p50']}/{spot_stats['p75']} " 

1241 f"(min {spot_stats['min']}, max {spot_stats['max']})" 

1242 ) 

1243 price_stats = stats["metrics"].get("spot_price") 

1244 if price_stats: 1244 ↛ 1245line 1244 didn't jump to line 1245 because the condition on line 1244 was never true

1245 print( 

1246 f" spot_price p25/p50/p75: " 

1247 f"{price_stats['p25']}/{price_stats['p50']}/{price_stats['p75']}" 

1248 ) 

1249 

1250 

1251def _format_patterns_grid(patterns: dict[str, Any]) -> str: 

1252 """Render a day-of-week x hour heatmap of average scores.""" 

1253 from ..capacity.history import DAY_NAMES 

1254 

1255 grid = patterns.get("patterns", {}) 

1256 metric = patterns.get("metric", "spot_score") 

1257 lines = [f"Average {metric} by day-of-week and hour (UTC)"] 

1258 header = "Day".ljust(10) + "".join(f"{hour:>5}" for hour in range(24)) 

1259 lines.append(header) 

1260 lines.append("-" * len(header)) 

1261 for day in DAY_NAMES: 

1262 hours = grid.get(day, {}) 

1263 row = day[:9].ljust(10) 

1264 for hour in range(24): 

1265 cell = hours.get(hour) or hours.get(str(hour)) 

1266 row += f"{cell['avg']:>5.1f}" if cell else f"{'.':>5}" 

1267 lines.append(row) 

1268 best = patterns.get("best_windows", [])[:3] 

1269 if best: 1269 ↛ 1277line 1269 didn't jump to line 1277 because the condition on line 1269 was always true

1270 lines.append("") 

1271 lines.append("Best windows:") 

1272 for window in best: 

1273 lines.append( 

1274 f"- {window['day']} {window['hour']:02d}:00 UTC " 

1275 f"avg={window['avg']} (n={window['count']})" 

1276 ) 

1277 return "\n".join(lines) 

1278 

1279 

1280@capacity.group("history") 

1281def history() -> None: 

1282 """Query the historical capacity surface (requires historical.enabled).""" 

1283 

1284 

1285@history.command("show") 

1286@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

1287@click.option("--region", "-r", required=True, help="AWS region") 

1288@click.option("--hours", "-H", default=168, help="Hours of history (default 168 = 7 days)") 

1289@pass_config 

1290def history_show(config: Any, instance_type: Any, region: Any, hours: Any) -> None: 

1291 """Show the capacity time-series for an instance type in a region.""" 

1292 from ..capacity.history import get_capacity_history_store 

1293 

1294 formatter = get_output_formatter(config) 

1295 try: 

1296 trend = get_capacity_history_store().get_trend(instance_type, region, hours) 

1297 if not trend: 

1298 formatter.print_warning( 

1299 f"No historical samples for {instance_type} in {region} in the last {hours}h yet (the poller records one about every 15 minutes)." 

1300 ) 

1301 return 

1302 formatter.print( 

1303 trend, 

1304 columns=[ 

1305 "timestamp", 

1306 "spot_score", 

1307 "spot_price", 

1308 "az_count", 

1309 "queue_depth", 

1310 "capacity_blocks_available", 

1311 "capacity_blocks_total", 

1312 "capacity_blocks_long_available", 

1313 "capacity_blocks_long_total", 

1314 ], 

1315 ) 

1316 except Exception as e: 

1317 if _history_disabled(e): 1317 ↛ 1320line 1317 didn't jump to line 1320 because the condition on line 1317 was always true

1318 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1319 return 

1320 formatter.print_error(f"Failed to load capacity history: {e}") 

1321 sys.exit(1) 

1322 

1323 

1324@history.command("stats") 

1325@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

1326@click.option("--region", "-r", required=True, help="AWS region") 

1327@click.option("--hours", "-H", default=168, help="Hours of history (default 168 = 7 days)") 

1328@pass_config 

1329def history_stats(config: Any, instance_type: Any, region: Any, hours: Any) -> None: 

1330 """Show a statistical summary (p25/p50/p75/min/max/stddev) per metric.""" 

1331 from ..capacity.history import get_capacity_history_store 

1332 

1333 formatter = get_output_formatter(config) 

1334 try: 

1335 stats = get_capacity_history_store().get_statistics(instance_type, region, hours) 

1336 if stats["sample_count"] == 0: 

1337 formatter.print_warning( 

1338 f"No historical samples for {instance_type} in {region} in the last {hours}h yet (the poller records one about every 15 minutes)." 

1339 ) 

1340 return 

1341 if config.output_format == "table": 1341 ↛ 1360line 1341 didn't jump to line 1360 because the condition on line 1341 was always true

1342 rows = [{"metric": name, **values} for name, values in stats["metrics"].items()] 

1343 print( 

1344 formatter.format( 

1345 rows, 

1346 columns=[ 

1347 "metric", 

1348 "count", 

1349 "min", 

1350 "p25", 

1351 "p50", 

1352 "p75", 

1353 "max", 

1354 "mean", 

1355 "stddev", 

1356 ], 

1357 ) 

1358 ) 

1359 else: 

1360 formatter.print(stats) 

1361 except Exception as e: 

1362 if _history_disabled(e): 1362 ↛ 1365line 1362 didn't jump to line 1365 because the condition on line 1362 was always true

1363 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1364 return 

1365 formatter.print_error(f"Failed to compute capacity statistics: {e}") 

1366 sys.exit(1) 

1367 

1368 

1369@history.command("patterns") 

1370@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

1371@click.option("--region", "-r", required=True, help="AWS region") 

1372@click.option("--hours", "-H", default=168, help="Hours of history (default 168 = 7 days)") 

1373@pass_config 

1374def history_patterns(config: Any, instance_type: Any, region: Any, hours: Any) -> None: 

1375 """Show a day/hour heatmap grid of average spot scores.""" 

1376 from ..capacity.history import get_capacity_history_store 

1377 

1378 formatter = get_output_formatter(config) 

1379 try: 

1380 patterns = get_capacity_history_store().get_temporal_patterns(instance_type, region, hours) 

1381 if not patterns["patterns"]: 1381 ↛ 1382line 1381 didn't jump to line 1382 because the condition on line 1381 was never true

1382 formatter.print_warning( 

1383 f"No historical samples for {instance_type} in {region} in the last {hours}h yet (the poller records one about every 15 minutes)." 

1384 ) 

1385 return 

1386 if config.output_format == "table": 1386 ↛ 1389line 1386 didn't jump to line 1389 because the condition on line 1386 was always true

1387 print(_format_patterns_grid(patterns)) 

1388 else: 

1389 formatter.print(patterns) 

1390 except Exception as e: 

1391 if _history_disabled(e): 1391 ↛ 1394line 1391 didn't jump to line 1394 because the condition on line 1391 was always true

1392 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1393 return 

1394 formatter.print_error(f"Failed to compute capacity patterns: {e}") 

1395 sys.exit(1) 

1396 

1397 

1398def _prediction_to_dict(prediction: Any) -> dict[str, Any]: 

1399 """Serialize a CapacityPredictionResult for non-table output.""" 

1400 return { 

1401 "instance_type": prediction.instance_type, 

1402 "region": prediction.region, 

1403 "confidence": prediction.confidence, 

1404 "best_windows": prediction.best_windows, 

1405 "avoid_windows": prediction.avoid_windows, 

1406 "reasoning": prediction.reasoning, 

1407 } 

1408 

1409 

1410def _print_prediction(prediction: Any, raw: bool) -> None: 

1411 """Render a single capacity-window prediction as a table block.""" 

1412 print() 

1413 print( 

1414 f" Best time to acquire {prediction.instance_type} in {prediction.region} " 

1415 f"(confidence: {prediction.confidence.upper()})" 

1416 ) 

1417 print(" " + "-" * 68) 

1418 if prediction.best_windows: 

1419 for window in prediction.best_windows[:5]: 

1420 print( 

1421 f" + {window.get('day', '?')} {window.get('hour_range', '?')}: " 

1422 f"{window.get('why', '')}" 

1423 ) 

1424 else: 

1425 print(" (no clear best window identified)") 

1426 if prediction.avoid_windows: 

1427 print() 

1428 print(" Windows to avoid:") 

1429 for window in prediction.avoid_windows[:5]: 

1430 print( 

1431 f" - {window.get('day', '?')} {window.get('hour_range', '?')}: " 

1432 f"{window.get('why', '')}" 

1433 ) 

1434 if prediction.reasoning: 

1435 print() 

1436 print(" Reasoning:") 

1437 for line in prediction.reasoning.split(". "): 

1438 if line.strip(): 1438 ↛ 1437line 1438 didn't jump to line 1437 because the condition on line 1438 was always true

1439 print(f" {line.strip()}") 

1440 if raw: 1440 ↛ 1441line 1440 didn't jump to line 1441 because the condition on line 1440 was never true

1441 print() 

1442 print(prediction.raw_response) 

1443 

1444 

1445@capacity.command("predict") 

1446@click.option("--instance-type", "-i", required=True, help="EC2 instance type") 

1447@click.option("--region", "-r", help="AWS region (omit when using --all-regions)") 

1448@click.option( 

1449 "--all-regions", 

1450 "-a", 

1451 is_flag=True, 

1452 help="Predict across every region that has historical data for the instance type", 

1453) 

1454@click.option( 

1455 "--hours", "-H", default=168, help="Hours of history to analyze (default 168 = 7 days)" 

1456) 

1457@click.option( 

1458 "--model", 

1459 "-m", 

1460 default=None, 

1461 help="Bedrock model ID to use (default: cdk.json context.bedrock.default_model_id).", 

1462) 

1463@click.option("--raw", is_flag=True, help="Show the raw AI response") 

1464@pass_config 

1465def predict_capacity( 

1466 config: Any, 

1467 instance_type: Any, 

1468 region: Any, 

1469 all_regions: Any, 

1470 hours: Any, 

1471 model: Any, 

1472 raw: Any, 

1473) -> None: 

1474 """Predict the best time to acquire capacity from historical patterns (Bedrock). 

1475 

1476 Combines the historical capacity surface (an optional add-on to the global 

1477 stack) with Amazon Bedrock to recommend the day/hour windows with the best 

1478 spot availability and pricing. Requires historical.enabled and collected 

1479 samples. Pass --all-regions to run the prediction for every region that has 

1480 data for the instance type instead of a single --region. 

1481 """ 

1482 from ..capacity import get_bedrock_capacity_advisor 

1483 

1484 formatter = get_output_formatter(config) 

1485 if all_regions and region: 

1486 formatter.print_error("Pass either --region or --all-regions, not both.") 

1487 sys.exit(1) 

1488 if not all_regions and not region: 

1489 formatter.print_error("Provide --region <region> or --all-regions.") 

1490 sys.exit(1) 

1491 

1492 try: 

1493 advisor = get_bedrock_capacity_advisor(config, model_id=model) 

1494 if all_regions: 

1495 predictions = advisor.predict_capacity_windows_all_regions( 

1496 instance_type, hours_back=hours 

1497 ) 

1498 else: 

1499 predictions = [advisor.predict_capacity_window(instance_type, region, hours_back=hours)] 

1500 except ValueError as e: 

1501 formatter.print_warning(str(e)) 

1502 return 

1503 except Exception as e: 

1504 if _history_disabled(e): 1504 ↛ 1507line 1504 didn't jump to line 1507 because the condition on line 1504 was always true

1505 formatter.print_warning(_HISTORY_DISABLED_HINT) 

1506 return 

1507 if is_bedrock_ftu_form_error(e): 

1508 formatter.print_error(BEDROCK_FTU_REMEDIATION) 

1509 sys.exit(1) 

1510 formatter.print_error(f"Failed to predict capacity window: {e}") 

1511 sys.exit(1) 

1512 

1513 if not predictions: 1513 ↛ 1514line 1513 didn't jump to line 1514 because the condition on line 1513 was never true

1514 formatter.print_warning( 

1515 f"No usable historical samples for {instance_type} in any region yet." 

1516 ) 

1517 return 

1518 

1519 if config.output_format != "table": 1519 ↛ 1520line 1519 didn't jump to line 1520 because the condition on line 1519 was never true

1520 payload = [_prediction_to_dict(p) for p in predictions] 

1521 formatter.print(payload if all_regions else payload[0]) 

1522 return 

1523 

1524 if all_regions: 

1525 formatter.print_info( 

1526 f"Predicted acquisition windows for {instance_type} across " 

1527 f"{len(predictions)} region(s) with data:" 

1528 ) 

1529 for prediction in predictions: 

1530 _print_prediction(prediction, raw)