flights 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fli/__init__.py ADDED
File without changes
fli/app/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from pathlib import Path
2
+
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv(Path(__file__).parents[1].joinpath(".env"))
fli/app/main.py ADDED
@@ -0,0 +1,197 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ import pandas as pd
4
+ import streamlit as st
5
+
6
+ from fli.app.utils import format_enum
7
+ from fli.models import (
8
+ Airport,
9
+ DateSearchFilters,
10
+ FlightSegment,
11
+ MaxStops,
12
+ PassengerInfo,
13
+ SeatType,
14
+ SortBy,
15
+ )
16
+ from fli.search.dates import SearchDates
17
+ from fli.search.flights import SearchFlights, SearchFlightsFilters
18
+
19
+ # Set page config
20
+ st.set_page_config(page_title="Fli", page_icon=":airplane:", layout="wide")
21
+
22
+ # Add title and description
23
+ st.title("✈️ Fli")
24
+ st.markdown("""
25
+ Search for flights using Google Flights data.
26
+ Enter your travel details below to find available flights.
27
+ """)
28
+
29
+ # Create first row of inputs
30
+ col1, col2, col3 = st.columns(3)
31
+
32
+ with col1:
33
+ # Origin airport
34
+ departure_airport = st.selectbox(
35
+ "From",
36
+ options=list(Airport),
37
+ format_func=lambda x: f"{x.name} ({x.value})",
38
+ index=list(Airport).index(Airport.SFO),
39
+ )
40
+
41
+ with col2:
42
+ # Destination airport
43
+ arrival_airport = st.selectbox(
44
+ "To",
45
+ options=list(Airport),
46
+ format_func=lambda x: f"{x.name} ({x.value})",
47
+ index=list(Airport).index(Airport.JFK),
48
+ )
49
+
50
+ with col3:
51
+ # Departure date
52
+ min_date = datetime.now().date()
53
+ max_date = min_date + timedelta(days=365)
54
+ default_date = min_date + timedelta(days=30)
55
+ departure_date = st.date_input(
56
+ "Departure Date", min_value=min_date, max_value=max_date, value=default_date
57
+ )
58
+
59
+ # Create second row of inputs
60
+ col4, col5, col6 = st.columns(3)
61
+
62
+ with col4:
63
+ # Maximum stops
64
+ stops = st.selectbox("Maximum Stops", options=list(MaxStops), format_func=format_enum)
65
+
66
+ with col5:
67
+ # Seat type
68
+ seat_type = st.selectbox("Cabin Class", options=list(SeatType), format_func=format_enum)
69
+
70
+ with col6:
71
+ # Number of passengers
72
+ num_adults = st.number_input("Number of Adults", min_value=1, max_value=9, value=1)
73
+
74
+ # Sort criteria
75
+ sort_by = st.selectbox("Sort Results By", options=list(SortBy), format_func=format_enum)
76
+
77
+ # Search button
78
+ if st.button("Search Flights", type="primary"):
79
+ # Input validation
80
+ if departure_airport == arrival_airport:
81
+ st.error("Origin and destination airports cannot be the same.")
82
+ else:
83
+ try:
84
+ # Create search filters
85
+ filters = SearchFlightsFilters(
86
+ departure_airport=departure_airport,
87
+ arrival_airport=arrival_airport,
88
+ departure_date=departure_date.strftime("%Y-%m-%d"),
89
+ passenger_info=PassengerInfo(adults=num_adults),
90
+ seat_type=seat_type,
91
+ stops=stops,
92
+ sort_by=sort_by,
93
+ )
94
+
95
+ # Show searching message
96
+ with st.spinner("Searching for flights..."):
97
+ # Perform search
98
+ search = SearchFlights()
99
+ results = search.search(filters)
100
+
101
+ # Search for date prices (4 weeks before and after)
102
+ date_from = departure_date - timedelta(weeks=4)
103
+ date_to = departure_date + timedelta(weeks=4)
104
+
105
+ date_filters = DateSearchFilters(
106
+ departure_airport=departure_airport,
107
+ arrival_airport=arrival_airport,
108
+ from_date=date_from.strftime("%Y-%m-%d"),
109
+ to_date=date_to.strftime("%Y-%m-%d"),
110
+ passenger_info=PassengerInfo(adults=num_adults),
111
+ seat_type=seat_type,
112
+ stops=stops,
113
+ flight_segments=[
114
+ FlightSegment(
115
+ departure_airport=[[departure_airport, 0]],
116
+ arrival_airport=[[arrival_airport, 0]],
117
+ travel_date=date_from.strftime("%Y-%m-%d"),
118
+ )
119
+ ],
120
+ )
121
+
122
+ date_search = SearchDates()
123
+ date_results = date_search.search(date_filters)
124
+
125
+ if results:
126
+ # Convert results to DataFrame for better display
127
+ flights_data = []
128
+ for flight in results:
129
+ for leg in flight.legs:
130
+ flights_data.append(
131
+ {
132
+ "Airline": leg.airline.value,
133
+ "Flight": leg.flight_number,
134
+ "From": leg.departure_airport.value,
135
+ "To": leg.arrival_airport.value,
136
+ "Departure": leg.departure_datetime.strftime("%H:%M"),
137
+ "Arrival": leg.arrival_datetime.strftime("%H:%M"),
138
+ "Duration": f"{leg.duration} mins",
139
+ "Price": f"${flight.price:,.2f}",
140
+ "Stops": flight.stops,
141
+ }
142
+ )
143
+
144
+ df = pd.DataFrame(flights_data)
145
+
146
+ # Display results in a nice table
147
+ st.subheader(f"Found {len(results)} flights")
148
+ st.dataframe(
149
+ df,
150
+ column_config={
151
+ "Airline": st.column_config.TextColumn("Airline"),
152
+ "Flight": st.column_config.TextColumn("Flight #"),
153
+ "From": st.column_config.TextColumn("From"),
154
+ "To": st.column_config.TextColumn("To"),
155
+ "Departure": st.column_config.TextColumn("Departure"),
156
+ "Arrival": st.column_config.TextColumn("Arrival"),
157
+ "Duration": st.column_config.TextColumn("Duration"),
158
+ "Price": st.column_config.TextColumn("Price"),
159
+ "Stops": st.column_config.NumberColumn("Stops"),
160
+ },
161
+ hide_index=True,
162
+ use_container_width=True,
163
+ )
164
+
165
+ # Display price trend chart
166
+ if date_results:
167
+ st.subheader("Price Trends")
168
+ date_prices = {result.date.date(): result.price for result in date_results}
169
+
170
+ df_prices = pd.DataFrame(
171
+ list(date_prices.items()), columns=["Date", "Price"]
172
+ ).set_index("Date")
173
+
174
+ st.line_chart(df_prices, use_container_width=True)
175
+
176
+ # Show min/max prices
177
+ min_price = min(date_prices.values())
178
+ max_price = max(date_prices.values())
179
+ min_date = min(date_prices.items(), key=lambda x: x[1])[0]
180
+
181
+ st.info(
182
+ "💰 Lowest price: ${:,.2f} on {}".format(
183
+ min_price, min_date.strftime("%B %d, %Y")
184
+ )
185
+ )
186
+ st.info(
187
+ "📈 Highest price: ${:,.2f} on {}".format(
188
+ max_price,
189
+ max(date_prices.items(), key=lambda x: x[1])[0].strftime("%B %d, %Y"),
190
+ )
191
+ )
192
+ else:
193
+ st.warning(
194
+ "No flights found for the selected criteria. Try different dates or airports."
195
+ )
196
+ except Exception as e:
197
+ st.error(f"An error occurred while searching for flights: {str(e)}")
fli/app/utils.py ADDED
@@ -0,0 +1,6 @@
1
+ from enum import Enum
2
+
3
+
4
+ def format_enum(enum: Enum) -> str:
5
+ """Format an enum value for display in a dropdown."""
6
+ return " ".join(enum.name.split("_")).title()
fli/cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """CLI module for the fli package."""
2
+
3
+ from fli.cli.main import cli
4
+
5
+ __all__ = ["cli"]
@@ -0,0 +1,6 @@
1
+ """Command functions for the CLI."""
2
+
3
+ from fli.cli.commands.cheap import cheap
4
+ from fli.cli.commands.search import search
5
+
6
+ __all__ = ["search", "cheap"]
@@ -0,0 +1,218 @@
1
+ from datetime import datetime, timedelta
2
+ from typing import Annotated
3
+
4
+ import typer
5
+
6
+ from fli.cli.enums import DayOfWeek
7
+ from fli.cli.utils import (
8
+ display_date_results,
9
+ filter_dates_by_days,
10
+ parse_airlines,
11
+ parse_stops,
12
+ validate_date,
13
+ validate_time_range,
14
+ )
15
+ from fli.models import (
16
+ Airport,
17
+ DateSearchFilters,
18
+ FlightSegment,
19
+ PassengerInfo,
20
+ SeatType,
21
+ TimeRestrictions,
22
+ )
23
+ from fli.search import SearchDates
24
+
25
+
26
+ def cheap(
27
+ from_airport: Annotated[str, typer.Argument(help="Departure airport code (e.g., JFK)")],
28
+ to_airport: Annotated[str, typer.Argument(help="Arrival airport code (e.g., LHR)")],
29
+ from_date: Annotated[
30
+ str, typer.Option("--from", help="Start date (YYYY-MM-DD)", callback=validate_date)
31
+ ] = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d"),
32
+ to_date: Annotated[
33
+ str, typer.Option("--to", help="End date (YYYY-MM-DD)", callback=validate_date)
34
+ ] = (datetime.now() + timedelta(days=60)).strftime("%Y-%m-%d"),
35
+ airlines: Annotated[
36
+ list[str] | None,
37
+ typer.Option(
38
+ "--airlines",
39
+ "-a",
40
+ help="List of airline codes (e.g., BA KL)",
41
+ ),
42
+ ] = None,
43
+ seat: Annotated[
44
+ str,
45
+ typer.Option(
46
+ "--class",
47
+ "-c",
48
+ help="Seat type (ECONOMY, PREMIUM_ECONOMY, BUSINESS, FIRST)",
49
+ ),
50
+ ] = "ECONOMY",
51
+ stops: Annotated[
52
+ str,
53
+ typer.Option(
54
+ "--stops",
55
+ "-s",
56
+ help="Maximum number of stops (ANY, 0 for non-stop, 1 for one stop, 2+ for two stops)",
57
+ ),
58
+ ] = "ANY",
59
+ sort: Annotated[
60
+ bool,
61
+ typer.Option(
62
+ "--sort",
63
+ help="Sort results by price (lowest to highest)",
64
+ ),
65
+ ] = False,
66
+ monday: Annotated[
67
+ bool,
68
+ typer.Option(
69
+ "--monday",
70
+ "-mon",
71
+ help="Include Mondays in results",
72
+ ),
73
+ ] = False,
74
+ tuesday: Annotated[
75
+ bool,
76
+ typer.Option(
77
+ "--tuesday",
78
+ "-tue",
79
+ help="Include Tuesdays in results",
80
+ ),
81
+ ] = False,
82
+ wednesday: Annotated[
83
+ bool,
84
+ typer.Option(
85
+ "--wednesday",
86
+ "-wed",
87
+ help="Include Wednesdays in results",
88
+ ),
89
+ ] = False,
90
+ thursday: Annotated[
91
+ bool,
92
+ typer.Option(
93
+ "--thursday",
94
+ "-thu",
95
+ help="Include Thursdays in results",
96
+ ),
97
+ ] = False,
98
+ friday: Annotated[
99
+ bool,
100
+ typer.Option(
101
+ "--friday",
102
+ "-fri",
103
+ help="Include Fridays in results",
104
+ ),
105
+ ] = False,
106
+ saturday: Annotated[
107
+ bool,
108
+ typer.Option(
109
+ "--saturday",
110
+ "-sat",
111
+ help="Include Saturdays in results",
112
+ ),
113
+ ] = False,
114
+ sunday: Annotated[
115
+ bool,
116
+ typer.Option(
117
+ "--sunday",
118
+ "-sun",
119
+ help="Include Sundays in results",
120
+ ),
121
+ ] = False,
122
+ time: Annotated[
123
+ str | None,
124
+ typer.Option(
125
+ "--time",
126
+ "-t",
127
+ help="Time range in 24h format (e.g., 6-20)",
128
+ callback=validate_time_range,
129
+ ),
130
+ ] = None,
131
+ ):
132
+ """Find the cheapest dates to fly between two airports.
133
+
134
+ Example:
135
+ fli cheap LAX MIA --seat BUSINESS --stops NON_STOP --friday
136
+
137
+ """
138
+ try:
139
+ # Parse parameters
140
+ departure_airport = getattr(Airport, from_airport.upper())
141
+ arrival_airport = getattr(Airport, to_airport.upper())
142
+ seat_type = getattr(SeatType, seat.upper())
143
+ max_stops = parse_stops(stops)
144
+
145
+ # Parse time restrictions
146
+ time_restrictions = None
147
+ if time:
148
+ if isinstance(time, tuple):
149
+ start_hour, end_hour = time
150
+ else:
151
+ start_hour, end_hour = map(int, time.split("-"))
152
+ time_restrictions = TimeRestrictions(
153
+ earliest_departure=start_hour,
154
+ latest_departure=end_hour,
155
+ earliest_arrival=None,
156
+ latest_arrival=None,
157
+ )
158
+
159
+ # Create flight segment
160
+ flight_segment = FlightSegment(
161
+ departure_airport=[[departure_airport, 0]],
162
+ arrival_airport=[[arrival_airport, 0]],
163
+ travel_date=from_date,
164
+ time_restrictions=time_restrictions,
165
+ )
166
+
167
+ # Create search filters
168
+ filters = DateSearchFilters(
169
+ passenger_info=PassengerInfo(adults=1),
170
+ flight_segments=[flight_segment],
171
+ stops=max_stops,
172
+ seat_type=seat_type,
173
+ airlines=parse_airlines(airlines),
174
+ from_date=from_date,
175
+ to_date=to_date,
176
+ )
177
+
178
+ # Perform search
179
+ search_client = SearchDates()
180
+ dates = search_client.search(filters)
181
+
182
+ if not dates:
183
+ typer.echo("No flights found for these dates.")
184
+ raise typer.Exit(1)
185
+
186
+ # Filter by days if any day filters are specified
187
+ selected_days = []
188
+ if monday:
189
+ selected_days.append(DayOfWeek.MONDAY)
190
+ if tuesday:
191
+ selected_days.append(DayOfWeek.TUESDAY)
192
+ if wednesday:
193
+ selected_days.append(DayOfWeek.WEDNESDAY)
194
+ if thursday:
195
+ selected_days.append(DayOfWeek.THURSDAY)
196
+ if friday:
197
+ selected_days.append(DayOfWeek.FRIDAY)
198
+ if saturday:
199
+ selected_days.append(DayOfWeek.SATURDAY)
200
+ if sunday:
201
+ selected_days.append(DayOfWeek.SUNDAY)
202
+
203
+ dates = filter_dates_by_days(dates, selected_days)
204
+
205
+ if not dates:
206
+ typer.echo("No flights found for the selected days.")
207
+ raise typer.Exit(1)
208
+
209
+ # Sort dates by price if sort flag is enabled
210
+ if sort:
211
+ dates.sort(key=lambda x: x.price)
212
+
213
+ # Display results
214
+ display_date_results(dates)
215
+
216
+ except (AttributeError, ValueError) as e:
217
+ typer.echo(f"Error: {str(e)}")
218
+ raise typer.Exit(1) from e
@@ -0,0 +1,135 @@
1
+ from typing import Annotated
2
+
3
+ import typer
4
+
5
+ from fli.cli.utils import (
6
+ display_flight_results,
7
+ filter_flights_by_airlines,
8
+ filter_flights_by_time,
9
+ parse_airlines,
10
+ parse_stops,
11
+ validate_date,
12
+ validate_time_range,
13
+ )
14
+ from fli.models import Airport, PassengerInfo, SeatType, SortBy
15
+ from fli.search import SearchFlights, SearchFlightsFilters
16
+
17
+
18
+ def search_flights(
19
+ from_airport: str,
20
+ to_airport: str,
21
+ date: str,
22
+ time: tuple[int, int] | None = None,
23
+ airlines: list[str] | None = None,
24
+ seat: str = "ECONOMY",
25
+ stops: str = "ANY",
26
+ sort: str = "CHEAPEST",
27
+ ):
28
+ """Core flight search functionality."""
29
+ try:
30
+ # Parse parameters
31
+ departure_airport = getattr(Airport, from_airport.upper())
32
+ arrival_airport = getattr(Airport, to_airport.upper())
33
+ seat_type = getattr(SeatType, seat.upper())
34
+ max_stops = parse_stops(stops)
35
+ sort_by = getattr(SortBy, sort.upper())
36
+
37
+ # Create search filters
38
+ filters = SearchFlightsFilters(
39
+ departure_airport=departure_airport,
40
+ arrival_airport=arrival_airport,
41
+ departure_date=date,
42
+ passenger_info=PassengerInfo(adults=1),
43
+ seat_type=seat_type,
44
+ stops=max_stops,
45
+ sort_by=sort_by,
46
+ )
47
+
48
+ # Perform search
49
+ search_client = SearchFlights()
50
+ flights = search_client.search(filters)
51
+
52
+ if not flights:
53
+ typer.echo("No flights found.")
54
+ raise typer.Exit(1)
55
+
56
+ # Apply time filter if specified
57
+ if time:
58
+ start_hour, end_hour = time
59
+ flights = filter_flights_by_time(flights, start_hour, end_hour)
60
+
61
+ # Apply airline filter if specified
62
+ airline_list = parse_airlines(airlines)
63
+ if airline_list:
64
+ flights = filter_flights_by_airlines(flights, airline_list)
65
+
66
+ # Display results
67
+ display_flight_results(flights)
68
+
69
+ except (AttributeError, ValueError) as e:
70
+ typer.echo(f"Error: {str(e)}")
71
+ raise typer.Exit(1) from e
72
+
73
+
74
+ def search(
75
+ from_airport: Annotated[str, typer.Argument(help="Departure airport code (e.g., JFK)")],
76
+ to_airport: Annotated[str, typer.Argument(help="Arrival airport code (e.g., LHR)")],
77
+ date: Annotated[str, typer.Argument(help="Travel date (YYYY-MM-DD)", callback=validate_date)],
78
+ time: Annotated[
79
+ str | None,
80
+ typer.Option(
81
+ "--time",
82
+ "-t",
83
+ help="Time range in 24h format (e.g., 6-20)",
84
+ callback=validate_time_range,
85
+ ),
86
+ ] = None,
87
+ airlines: Annotated[
88
+ list[str] | None,
89
+ typer.Option(
90
+ "--airlines",
91
+ "-a",
92
+ help="List of airline codes (e.g., BA KL)",
93
+ ),
94
+ ] = None,
95
+ seat: Annotated[
96
+ str,
97
+ typer.Option(
98
+ "--class",
99
+ "-c",
100
+ help="Seat type (ECONOMY, PREMIUM_ECONOMY, BUSINESS, FIRST)",
101
+ ),
102
+ ] = "ECONOMY",
103
+ stops: Annotated[
104
+ str,
105
+ typer.Option(
106
+ "--stops",
107
+ "-s",
108
+ help="Maximum number of stops (ANY, 0 for non-stop, 1 for one stop, 2+ for two stops)",
109
+ ),
110
+ ] = "ANY",
111
+ sort: Annotated[
112
+ str,
113
+ typer.Option(
114
+ "--sort",
115
+ "-o",
116
+ help="Sort results by (CHEAPEST, DURATION, DEPARTURE_TIME, ARRIVAL_TIME)",
117
+ ),
118
+ ] = "CHEAPEST",
119
+ ):
120
+ """Search for flights with flexible filtering options.
121
+
122
+ Example:
123
+ fli search JFK LHR 2025-10-25 --time 6-20 --airlines BA KL --stops NON_STOP
124
+
125
+ """
126
+ search_flights(
127
+ from_airport=from_airport,
128
+ to_airport=to_airport,
129
+ date=date,
130
+ time=time,
131
+ airlines=airlines,
132
+ seat=seat,
133
+ stops=stops,
134
+ sort=sort,
135
+ )
@@ -0,0 +1,16 @@
1
+ """App command implementation."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import streamlit.web.cli as stcli
7
+
8
+
9
+ def streamlit() -> None:
10
+ """Launch the Streamlit app interface."""
11
+ # Get the path to main.py
12
+ app_path = Path(__file__).parents[2].joinpath("app", "main.py")
13
+
14
+ # Run the Streamlit app
15
+ sys.argv = ["streamlit", "run", str(app_path), "--server.port", "3600"]
16
+ sys.exit(stcli.main())
fli/cli/console.py ADDED
@@ -0,0 +1,5 @@
1
+ """Console module for the CLI."""
2
+
3
+ from rich.console import Console
4
+
5
+ console = Console()
fli/cli/enums.py ADDED
@@ -0,0 +1,13 @@
1
+ from enum import Enum
2
+
3
+
4
+ class DayOfWeek(Enum):
5
+ """Days of the week."""
6
+
7
+ MONDAY = "monday"
8
+ TUESDAY = "tuesday"
9
+ WEDNESDAY = "wednesday"
10
+ THURSDAY = "thursday"
11
+ FRIDAY = "friday"
12
+ SATURDAY = "saturday"
13
+ SUNDAY = "sunday"
fli/cli/main.py ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import sys
4
+
5
+ import typer
6
+
7
+ from fli.cli.commands.cheap import cheap
8
+ from fli.cli.commands.search import search
9
+ from fli.cli.commands.streamlit import streamlit
10
+
11
+ app = typer.Typer(
12
+ help="Search for flights using Google Flights data",
13
+ add_completion=True,
14
+ )
15
+
16
+ # Register commands
17
+ app.command(name="app")(streamlit)
18
+ app.command(name="cheap")(cheap)
19
+ app.command(name="search")(search)
20
+
21
+
22
+ @app.callback(invoke_without_command=True)
23
+ def main(ctx: typer.Context):
24
+ """Search for flights using Google Flights data.
25
+
26
+ If no command is provided, the search command will be used.
27
+ """
28
+ # If no command is provided, show help
29
+ if ctx.invoked_subcommand is None:
30
+ ctx.get_help()
31
+ raise typer.Exit()
32
+
33
+
34
+ def cli():
35
+ """Entry point for the CLI that handles default command."""
36
+ args = sys.argv[1:]
37
+ if not args:
38
+ app()
39
+ return
40
+
41
+ # If the first argument isn't a command, treat as search
42
+ if args[0] not in ["app", "cheap", "search", "--help", "-h"]:
43
+ sys.argv.insert(1, "search")
44
+
45
+ app()
46
+
47
+
48
+ if __name__ == "__main__":
49
+ cli()