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 +0 -0
- fli/app/__init__.py +5 -0
- fli/app/main.py +197 -0
- fli/app/utils.py +6 -0
- fli/cli/__init__.py +5 -0
- fli/cli/commands/__init__.py +6 -0
- fli/cli/commands/cheap.py +218 -0
- fli/cli/commands/search.py +135 -0
- fli/cli/commands/streamlit.py +16 -0
- fli/cli/console.py +5 -0
- fli/cli/enums.py +13 -0
- fli/cli/main.py +49 -0
- fli/cli/utils.py +194 -0
- fli/models/__init__.py +33 -0
- fli/models/airline.py +1109 -0
- fli/models/airport.py +9775 -0
- fli/models/google_flights.py +442 -0
- fli/search/__init__.py +8 -0
- fli/search/client.py +94 -0
- fli/search/dates.py +153 -0
- fli/search/flights.py +216 -0
- flights-0.3.0.dist-info/LICENSE.txt +21 -0
- flights-0.3.0.dist-info/METADATA +208 -0
- flights-0.3.0.dist-info/RECORD +26 -0
- flights-0.3.0.dist-info/WHEEL +4 -0
- flights-0.3.0.dist-info/entry_points.txt +3 -0
fli/__init__.py
ADDED
|
File without changes
|
fli/app/__init__.py
ADDED
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
fli/cli/__init__.py
ADDED
|
@@ -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
fli/cli/enums.py
ADDED
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()
|