""" CLI Entry Point for Vacation Deal Finder Command-line interface for searching vacation deals """ import asyncio import argparse import json import sys from datetime import datetime, timedelta from holland_agent import VacationAgent # pyre-ignore[21] from html_report_generator import HTMLReportGenerator # pyre-ignore[21] def parse_args(): """Parse command-line arguments""" parser = argparse.ArgumentParser( description="Vacation Deal Finder - Find budget-friendly, dog-friendly accommodations globally", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Search Berlin for 7 nights in February python main.py --cities Berlin --checkin 2026-02-15 --checkout 2026-02-22 # Search multiple destinations with budget limit python main.py --cities "Amsterdam,Ardennes,Winterberg" --checkin 2026-02-15 --checkout 2026-02-22 --budget-max 200 # Search with custom group size python main.py --cities "Paris, France" --checkin 2026-03-01 --checkout 2026-03-08 --adults 2 --pets 1 """ ) parser.add_argument( "--cities", type=str, required=True, help="Comma-separated list of cities, regions, or countries (e.g., 'Amsterdam, Berlin, Ardennes')" ) parser.add_argument( "--checkin", type=str, required=True, help="Check-in date in YYYY-MM-DD format (e.g., '2026-02-15')" ) parser.add_argument( "--checkout", type=str, required=True, help="Check-out date in YYYY-MM-DD format (e.g., '2026-02-22')" ) parser.add_argument( "--budget-min", type=int, default=40, help="Minimum budget per night in EUR (default: 40)" ) parser.add_argument( "--budget-max", type=int, default=250, help="Maximum budget per night in EUR (default: 250)" ) parser.add_argument( "--adults", type=int, default=4, help="Number of adults (default: 4)" ) parser.add_argument( "--pets", type=int, default=1, help="Number of pets (default: 1)" ) parser.add_argument( "--output", type=str, choices=["json", "summary"], default="json", help="Output format: 'json' for full data, 'summary' for human-readable (default: json)" ) parser.add_argument( "--top", type=int, default=10, help="Number of top deals to show (default: 10)" ) parser.add_argument( "--report", type=str, choices=["none", "html"], default="html", help="Generate report file: 'html' for VacationDeals_YYYYMMDD.html (default: html)" ) parser.add_argument( "--schedule-minutes", type=int, default=0, help="Run search every N minutes (0 = run once and exit, default: 0)" ) parser.add_argument( "--max-runs", type=int, default=0, help="Maximum scheduled runs (only used with --schedule-minutes, 0 = unlimited)" ) return parser.parse_args() def validate_dates(checkin: str, checkout: str) -> bool: """Validate date format and logic""" try: checkin_date = datetime.strptime(checkin, "%Y-%m-%d") checkout_date = datetime.strptime(checkout, "%Y-%m-%d") if checkout_date <= checkin_date: print("Error: Check-out date must be after check-in date") return False if checkin_date < datetime.now(): print("Warning: Check-in date is in the past") return True except ValueError: print("Error: Invalid date format. Use YYYY-MM-DD (e.g., '2026-02-15')") return False def print_summary(results: dict, top_n: int): """Print human-readable summary""" print("\n" + "="*70) print("VACATION DEAL FINDER - RESULTS") print("="*70) # Search parameters params = results["search_params"] print(f"\nSearch Parameters:") print(f" Destinations: {', '.join(params['cities'])}") print(f" Dates: {params['checkin']} to {params['checkout']} ({params['nights']} nights)") print(f" Group: {params['group_size']} adults + {params['pets']} pet(s)") print(f" Budget: {params['budget_range']} per night") # Summary summary = results["summary"] print(f"\nSearch Summary:") print(f" Total properties found: {summary['total_options_found']}") print(f" Dog-friendly options: {summary['dog_friendly_options']}") print(f" Best overall: {summary['best_overall']}") print(f" Top rated: {summary['top_rated_property']}") print(f" Cheapest: {summary['cheapest_option']}") # Budget overview budget = summary["budget_overview"] print(f"\nPrice Range:") print(f" Cheapest: €{budget['cheapest_per_night']}/night") print(f" Average: €{budget['average_per_night']}/night") print(f" Most expensive: €{budget['most_expensive_per_night']}/night") # Top deals print(f"\n{'='*70}") print(f"TOP {top_n} DEALS") print("="*70) for i, deal in enumerate(results["top_10_deals"][:top_n], 1): print(f"\n#{i} - {deal['name']}") print(f" Location: {deal['location']}") print(f" Price: €{deal['price_per_night']}/night (€{deal['total_cost_for_trip']} total)") print(f" Rating: {deal['rating']}/5.0 ({deal['reviews']} reviews)") print(f" Pet-friendly: {'Yes' if deal['pet_friendly'] else 'No'}") print(f" Source: {deal['source']}") print(f" Score: {deal['rank_score']}/100") print(f" {deal['recommendation']}") # Direct link if deal.get("url"): print(f" 🔗 {deal['url']}") # Weather info if available if deal.get("weather_forecast"): weather = deal["weather_forecast"] if weather.get("avg_temp"): print(f" Weather: {weather['avg_temp']}°C avg, {weather.get('conditions', 'N/A')}") print("\n" + "="*70) async def run_once(agent: VacationAgent, args, cities, run_index: int = 1): """Execute one search cycle and handle output/report generation.""" results = await agent.find_best_deals( cities=cities, checkin=args.checkin, checkout=args.checkout, group_size=args.adults, pets=args.pets ) if args.output == "json": print("\n" + json.dumps(results, indent=2, ensure_ascii=False)) else: print_summary(results, args.top) if args.report == "html": report_gen = HTMLReportGenerator() timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"VacationDeals_{timestamp}_run{run_index}.html" path = report_gen.generate_report( deals=results["top_10_deals"], search_params=results["search_params"], filename=filename ) print(f"\n📊 Report generated: {path}") async def main(): """Main CLI entry point""" args = parse_args() if not validate_dates(args.checkin, args.checkout): sys.exit(1) cities = [city.strip() for city in args.cities.split(",")] agent = VacationAgent( budget_min=args.budget_min, budget_max=args.budget_max ) try: interval = max(0, int(args.schedule_minutes)) max_runs = max(0, int(args.max_runs)) if interval == 0: await run_once(agent, args, cities, run_index=1) return run_count = 0 print(f"\n⏱️ Scheduler active: every {interval} minute(s)") if max_runs > 0: print(f" Max runs: {max_runs}") while True: run_count += 1 started = datetime.now().strftime('%Y-%m-%d %H:%M:%S') print(f"\n🚀 Scheduled run #{run_count} started at {started}") try: await run_once(agent, args, cities, run_index=run_count) except Exception as cycle_error: print(f"\nRun #{run_count} failed: {cycle_error}", file=sys.stderr) if max_runs > 0 and run_count >= max_runs: print("\n✅ Scheduler finished: max runs reached") break next_run = datetime.now() + timedelta(minutes=interval) print( f"\n⏳ Waiting {interval} minute(s) until next run " f"({next_run.strftime('%Y-%m-%d %H:%M:%S')})" ) await asyncio.sleep(float(interval * 60)) except KeyboardInterrupt: print("\n\nSearch cancelled by user") sys.exit(130) except Exception as e: print(f"\nError: {e}", file=sys.stderr) sys.exit(1) finally: await agent.cleanup() if __name__ == "__main__": asyncio.run(main())