swoop-flights 0.1.0__tar.gz
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.
- swoop_flights-0.1.0/.gitignore +24 -0
- swoop_flights-0.1.0/LICENSE +21 -0
- swoop_flights-0.1.0/PKG-INFO +249 -0
- swoop_flights-0.1.0/README.md +222 -0
- swoop_flights-0.1.0/pyproject.toml +61 -0
- swoop_flights-0.1.0/swoop/__init__.py +358 -0
- swoop_flights-0.1.0/swoop/_booking.py +447 -0
- swoop_flights-0.1.0/swoop/_validate.py +178 -0
- swoop_flights-0.1.0/swoop/builders.py +199 -0
- swoop_flights-0.1.0/swoop/decoder.py +545 -0
- swoop_flights-0.1.0/swoop/exceptions.py +34 -0
- swoop_flights-0.1.0/swoop/flights.proto +54 -0
- swoop_flights-0.1.0/swoop/flights_pb2.py +50 -0
- swoop_flights-0.1.0/swoop/py.typed +0 -0
- swoop_flights-0.1.0/swoop/rpc.py +588 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.eggs/
|
|
9
|
+
|
|
10
|
+
# Hypothesis
|
|
11
|
+
.hypothesis/
|
|
12
|
+
|
|
13
|
+
# Test / coverage
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
htmlcov/
|
|
16
|
+
.coverage
|
|
17
|
+
|
|
18
|
+
# Environment
|
|
19
|
+
.env
|
|
20
|
+
.env.*
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.vscode/
|
|
24
|
+
.idea/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ayush Saraswat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swoop-flights
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Google Flights price scraper. Search flights programmatically via the same RPC endpoints the web app uses.
|
|
5
|
+
Project-URL: Homepage, https://github.com/saraswatayu/swoop
|
|
6
|
+
Project-URL: Issues, https://github.com/saraswatayu/swoop/issues
|
|
7
|
+
Author: Ayush Saraswat
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: flights,google-flights,prices,scraper,travel
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: primp>=1.0.0
|
|
23
|
+
Requires-Dist: protobuf>=4.0.0
|
|
24
|
+
Provides-Extra: validation
|
|
25
|
+
Requires-Dist: airportsdata>=1.3.0; extra == 'validation'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# swoop
|
|
29
|
+
|
|
30
|
+
Google Flights price scraper. Search flights programmatically using the same RPC endpoints the Google Flights web app uses.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from swoop import search
|
|
34
|
+
|
|
35
|
+
results = search("JFK", "LAX", "2026-06-01")
|
|
36
|
+
for flight in results.best:
|
|
37
|
+
airline = ", ".join(flight.airline_names)
|
|
38
|
+
print(f"${flight.price} — {airline}")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install swoop-flights
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### One-way search
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from swoop import search
|
|
53
|
+
|
|
54
|
+
results = search("SFO", "JFK", "2026-06-15")
|
|
55
|
+
|
|
56
|
+
# results.best — top-ranked flights
|
|
57
|
+
# results.other — remaining flights
|
|
58
|
+
for flight in results.best:
|
|
59
|
+
print(f"${flight.price}")
|
|
60
|
+
print(f" {flight.departure_airport} → {flight.arrival_airport}")
|
|
61
|
+
print(f" {flight.airline_names}, {flight.stop_count} stops")
|
|
62
|
+
print(f" {flight.travel_time} min total")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Roundtrip search
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
results = search("SFO", "JFK", "2026-06-15", return_date="2026-06-22")
|
|
69
|
+
# Price in results is the roundtrip total
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Cabin class and filters
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from swoop import search, SORT_CHEAPEST
|
|
76
|
+
|
|
77
|
+
results = search(
|
|
78
|
+
"LAX", "NRT", "2026-06-15",
|
|
79
|
+
cabin="business", # economy, premium-economy, business, first
|
|
80
|
+
max_stops=0, # nonstop only
|
|
81
|
+
sort=SORT_CHEAPEST, # cheapest first
|
|
82
|
+
airlines=["NH", "JL"], # filter to specific carriers
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Time window filtering
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
results = search(
|
|
90
|
+
"JFK", "LHR", "2026-06-15",
|
|
91
|
+
earliest_departure=8, # depart after 8am
|
|
92
|
+
latest_departure=14, # depart before 2pm
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Booking details (fare options)
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from swoop import search, get_booking_results
|
|
100
|
+
|
|
101
|
+
results = search("JFK", "LAX", "2026-06-15")
|
|
102
|
+
itinerary = results.best[0]
|
|
103
|
+
|
|
104
|
+
# Get fare tiers — just pass the itinerary
|
|
105
|
+
options = get_booking_results(itinerary)
|
|
106
|
+
|
|
107
|
+
for opt in options:
|
|
108
|
+
print(f"${opt.price} — {opt.brand_label} ({opt.fare_family})")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
You can also pass a booking token string with explicit parameters:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
options = get_booking_results(
|
|
115
|
+
itinerary.booking_token,
|
|
116
|
+
origin="JFK",
|
|
117
|
+
destination="LAX",
|
|
118
|
+
date="2026-06-15",
|
|
119
|
+
selected_legs=[
|
|
120
|
+
[
|
|
121
|
+
flight.departure_airport,
|
|
122
|
+
f"{flight.departure_date[0]}-{flight.departure_date[1]:02d}-{flight.departure_date[2]:02d}",
|
|
123
|
+
flight.arrival_airport,
|
|
124
|
+
None,
|
|
125
|
+
flight.airline,
|
|
126
|
+
flight.flight_number,
|
|
127
|
+
]
|
|
128
|
+
for flight in itinerary.flights
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Retry and timeout
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# Retry up to 3 times on HTTP 429 (rate limit) with exponential backoff
|
|
137
|
+
results = search("JFK", "LAX", "2026-06-15", retries=3, timeout=90)
|
|
138
|
+
|
|
139
|
+
# Same for booking results
|
|
140
|
+
options = get_booking_results(itinerary, retries=2, timeout=60)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Flight details
|
|
144
|
+
|
|
145
|
+
Each `Itinerary` contains detailed segment data:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
results = search("JFK", "LAX", "2026-06-15")
|
|
149
|
+
for itinerary in results.best:
|
|
150
|
+
for flight in itinerary.flights:
|
|
151
|
+
print(f"{flight.airline} {flight.flight_number}")
|
|
152
|
+
print(f" {flight.departure_airport} → {flight.arrival_airport}")
|
|
153
|
+
print(f" Aircraft: {flight.aircraft}")
|
|
154
|
+
print(f" Legroom: {flight.legroom}")
|
|
155
|
+
if flight.co2_grams:
|
|
156
|
+
print(f" CO₂: {flight.co2_grams}g")
|
|
157
|
+
|
|
158
|
+
if itinerary.carbon_emissions:
|
|
159
|
+
ce = itinerary.carbon_emissions
|
|
160
|
+
print(f" Route emissions: {ce.difference_percent}% vs typical")
|
|
161
|
+
|
|
162
|
+
if results.price_range:
|
|
163
|
+
print(f" Price range: ${results.price_range.low}–${results.price_range.high}")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Error handling
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from swoop import search, SwoopHTTPError, SwoopRateLimitError, SwoopParseError
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
results = search("JFK", "LAX", "2026-06-15")
|
|
173
|
+
except SwoopRateLimitError:
|
|
174
|
+
print("Rate limited — wait a few minutes")
|
|
175
|
+
except SwoopHTTPError as e:
|
|
176
|
+
print(f"HTTP {e.status_code}")
|
|
177
|
+
except SwoopParseError as e:
|
|
178
|
+
print(f"Parse error: {e}")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## API reference
|
|
182
|
+
|
|
183
|
+
### `search(origin, destination, date, **kwargs)`
|
|
184
|
+
|
|
185
|
+
Search Google Flights and return a `SearchResult`.
|
|
186
|
+
|
|
187
|
+
| Parameter | Type | Default | Description |
|
|
188
|
+
|-----------|------|---------|-------------|
|
|
189
|
+
| `origin` | `str` | required | Origin IATA code |
|
|
190
|
+
| `destination` | `str` | required | Destination IATA code |
|
|
191
|
+
| `date` | `str` | required | Departure date (`YYYY-MM-DD`) |
|
|
192
|
+
| `return_date` | `str \| None` | `None` | Return date for roundtrip |
|
|
193
|
+
| `cabin` | `str` | `"economy"` | `economy`, `premium-economy`, `business`, `first` |
|
|
194
|
+
| `adults` | `int` | `1` | Number of adults |
|
|
195
|
+
| `max_stops` | `int \| None` | `None` | `None`=any, `0`=nonstop, `1`=1 stop, `2`=2 stops |
|
|
196
|
+
| `sort` | `int` | `SORT_DEPARTURE_TIME` | Sort order constant |
|
|
197
|
+
| `airlines` | `list[str] \| None` | `None` | Filter by airline codes |
|
|
198
|
+
| `include_basic_economy` | `bool` | `False` | Include basic economy fares |
|
|
199
|
+
| `timeout` | `int` | `90` | HTTP timeout in seconds |
|
|
200
|
+
| `retries` | `int` | `0` | Retries on HTTP 429 with exponential backoff |
|
|
201
|
+
|
|
202
|
+
Returns `SearchResult | None`. `None` means no results found.
|
|
203
|
+
|
|
204
|
+
### `get_booking_results(itinerary_or_token, **kwargs)`
|
|
205
|
+
|
|
206
|
+
Get fare options for a specific itinerary. Pass an `Itinerary` object directly, or a booking token string with explicit `origin`, `destination`, `date`, and `selected_legs`. Returns `list[BookingOption]` with `price`, `brand_label`, `brand_code`, `fare_family`, etc. `BookingOption` supports both attribute access (`opt.price`) and dict-style access (`opt["price"]`, `opt.get("price")`).
|
|
207
|
+
|
|
208
|
+
### Result types
|
|
209
|
+
|
|
210
|
+
- **`SearchResult`** — `best: list[Itinerary]`, `other: list[Itinerary]`, `price_range: PriceRange | None`
|
|
211
|
+
- **`Itinerary`** — Full itinerary with `price`, `flights`, `layovers`, `travel_time`, `booking_token`, `carbon_emissions`
|
|
212
|
+
- **`Flight`** — Segment details: `airline`, `flight_number`, `aircraft`, `legroom`, `co2_grams`, `amenities`
|
|
213
|
+
- **`Layover`** — Stop info: `minutes`, airports, `is_overnight`
|
|
214
|
+
- **`CarbonEmissions`** — `this_flight_grams`, `typical_for_route_grams`, `difference_percent`
|
|
215
|
+
|
|
216
|
+
### Constants
|
|
217
|
+
|
|
218
|
+
| Constant | Value | Description |
|
|
219
|
+
|----------|-------|-------------|
|
|
220
|
+
| `SORT_TOP` | `1` | Google's default ranking |
|
|
221
|
+
| `SORT_CHEAPEST` | `2` | Cheapest first |
|
|
222
|
+
| `SORT_DEPARTURE_TIME` | `3` | By departure time |
|
|
223
|
+
| `SORT_ARRIVAL_TIME` | `4` | By arrival time |
|
|
224
|
+
| `SORT_DURATION` | `5` | Shortest first |
|
|
225
|
+
|
|
226
|
+
## Pricing notes
|
|
227
|
+
|
|
228
|
+
Use `itinerary.price` to get the USD price as an integer.
|
|
229
|
+
|
|
230
|
+
By default, `search()` excludes basic economy fares so prices reflect Main Cabin. Pass `include_basic_economy=True` to include them:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
results = search("JFK", "LAX", "2026-06-15", include_basic_economy=True)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## How it works
|
|
237
|
+
|
|
238
|
+
Swoop uses Google Flights' internal `GetShoppingResults` and `GetBookingResults` RPC endpoints — the same ones the web app calls when you search for flights. Requests are serialized as nested JSON payloads and sent via HTTP POST with browser impersonation (via [primp](https://github.com/deedy5/primp)).
|
|
239
|
+
|
|
240
|
+
Responses are decoded from nested list structures into typed Python dataclasses.
|
|
241
|
+
|
|
242
|
+
## Dependencies
|
|
243
|
+
|
|
244
|
+
- **[primp](https://github.com/deedy5/primp)** — HTTP client with browser TLS impersonation
|
|
245
|
+
- **[protobuf](https://pypi.org/project/protobuf/)** — Protocol buffer serialization
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# swoop
|
|
2
|
+
|
|
3
|
+
Google Flights price scraper. Search flights programmatically using the same RPC endpoints the Google Flights web app uses.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from swoop import search
|
|
7
|
+
|
|
8
|
+
results = search("JFK", "LAX", "2026-06-01")
|
|
9
|
+
for flight in results.best:
|
|
10
|
+
airline = ", ".join(flight.airline_names)
|
|
11
|
+
print(f"${flight.price} — {airline}")
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install swoop-flights
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### One-way search
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from swoop import search
|
|
26
|
+
|
|
27
|
+
results = search("SFO", "JFK", "2026-06-15")
|
|
28
|
+
|
|
29
|
+
# results.best — top-ranked flights
|
|
30
|
+
# results.other — remaining flights
|
|
31
|
+
for flight in results.best:
|
|
32
|
+
print(f"${flight.price}")
|
|
33
|
+
print(f" {flight.departure_airport} → {flight.arrival_airport}")
|
|
34
|
+
print(f" {flight.airline_names}, {flight.stop_count} stops")
|
|
35
|
+
print(f" {flight.travel_time} min total")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Roundtrip search
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
results = search("SFO", "JFK", "2026-06-15", return_date="2026-06-22")
|
|
42
|
+
# Price in results is the roundtrip total
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Cabin class and filters
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from swoop import search, SORT_CHEAPEST
|
|
49
|
+
|
|
50
|
+
results = search(
|
|
51
|
+
"LAX", "NRT", "2026-06-15",
|
|
52
|
+
cabin="business", # economy, premium-economy, business, first
|
|
53
|
+
max_stops=0, # nonstop only
|
|
54
|
+
sort=SORT_CHEAPEST, # cheapest first
|
|
55
|
+
airlines=["NH", "JL"], # filter to specific carriers
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Time window filtering
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
results = search(
|
|
63
|
+
"JFK", "LHR", "2026-06-15",
|
|
64
|
+
earliest_departure=8, # depart after 8am
|
|
65
|
+
latest_departure=14, # depart before 2pm
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Booking details (fare options)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from swoop import search, get_booking_results
|
|
73
|
+
|
|
74
|
+
results = search("JFK", "LAX", "2026-06-15")
|
|
75
|
+
itinerary = results.best[0]
|
|
76
|
+
|
|
77
|
+
# Get fare tiers — just pass the itinerary
|
|
78
|
+
options = get_booking_results(itinerary)
|
|
79
|
+
|
|
80
|
+
for opt in options:
|
|
81
|
+
print(f"${opt.price} — {opt.brand_label} ({opt.fare_family})")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
You can also pass a booking token string with explicit parameters:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
options = get_booking_results(
|
|
88
|
+
itinerary.booking_token,
|
|
89
|
+
origin="JFK",
|
|
90
|
+
destination="LAX",
|
|
91
|
+
date="2026-06-15",
|
|
92
|
+
selected_legs=[
|
|
93
|
+
[
|
|
94
|
+
flight.departure_airport,
|
|
95
|
+
f"{flight.departure_date[0]}-{flight.departure_date[1]:02d}-{flight.departure_date[2]:02d}",
|
|
96
|
+
flight.arrival_airport,
|
|
97
|
+
None,
|
|
98
|
+
flight.airline,
|
|
99
|
+
flight.flight_number,
|
|
100
|
+
]
|
|
101
|
+
for flight in itinerary.flights
|
|
102
|
+
],
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Retry and timeout
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# Retry up to 3 times on HTTP 429 (rate limit) with exponential backoff
|
|
110
|
+
results = search("JFK", "LAX", "2026-06-15", retries=3, timeout=90)
|
|
111
|
+
|
|
112
|
+
# Same for booking results
|
|
113
|
+
options = get_booking_results(itinerary, retries=2, timeout=60)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Flight details
|
|
117
|
+
|
|
118
|
+
Each `Itinerary` contains detailed segment data:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
results = search("JFK", "LAX", "2026-06-15")
|
|
122
|
+
for itinerary in results.best:
|
|
123
|
+
for flight in itinerary.flights:
|
|
124
|
+
print(f"{flight.airline} {flight.flight_number}")
|
|
125
|
+
print(f" {flight.departure_airport} → {flight.arrival_airport}")
|
|
126
|
+
print(f" Aircraft: {flight.aircraft}")
|
|
127
|
+
print(f" Legroom: {flight.legroom}")
|
|
128
|
+
if flight.co2_grams:
|
|
129
|
+
print(f" CO₂: {flight.co2_grams}g")
|
|
130
|
+
|
|
131
|
+
if itinerary.carbon_emissions:
|
|
132
|
+
ce = itinerary.carbon_emissions
|
|
133
|
+
print(f" Route emissions: {ce.difference_percent}% vs typical")
|
|
134
|
+
|
|
135
|
+
if results.price_range:
|
|
136
|
+
print(f" Price range: ${results.price_range.low}–${results.price_range.high}")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Error handling
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from swoop import search, SwoopHTTPError, SwoopRateLimitError, SwoopParseError
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
results = search("JFK", "LAX", "2026-06-15")
|
|
146
|
+
except SwoopRateLimitError:
|
|
147
|
+
print("Rate limited — wait a few minutes")
|
|
148
|
+
except SwoopHTTPError as e:
|
|
149
|
+
print(f"HTTP {e.status_code}")
|
|
150
|
+
except SwoopParseError as e:
|
|
151
|
+
print(f"Parse error: {e}")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## API reference
|
|
155
|
+
|
|
156
|
+
### `search(origin, destination, date, **kwargs)`
|
|
157
|
+
|
|
158
|
+
Search Google Flights and return a `SearchResult`.
|
|
159
|
+
|
|
160
|
+
| Parameter | Type | Default | Description |
|
|
161
|
+
|-----------|------|---------|-------------|
|
|
162
|
+
| `origin` | `str` | required | Origin IATA code |
|
|
163
|
+
| `destination` | `str` | required | Destination IATA code |
|
|
164
|
+
| `date` | `str` | required | Departure date (`YYYY-MM-DD`) |
|
|
165
|
+
| `return_date` | `str \| None` | `None` | Return date for roundtrip |
|
|
166
|
+
| `cabin` | `str` | `"economy"` | `economy`, `premium-economy`, `business`, `first` |
|
|
167
|
+
| `adults` | `int` | `1` | Number of adults |
|
|
168
|
+
| `max_stops` | `int \| None` | `None` | `None`=any, `0`=nonstop, `1`=1 stop, `2`=2 stops |
|
|
169
|
+
| `sort` | `int` | `SORT_DEPARTURE_TIME` | Sort order constant |
|
|
170
|
+
| `airlines` | `list[str] \| None` | `None` | Filter by airline codes |
|
|
171
|
+
| `include_basic_economy` | `bool` | `False` | Include basic economy fares |
|
|
172
|
+
| `timeout` | `int` | `90` | HTTP timeout in seconds |
|
|
173
|
+
| `retries` | `int` | `0` | Retries on HTTP 429 with exponential backoff |
|
|
174
|
+
|
|
175
|
+
Returns `SearchResult | None`. `None` means no results found.
|
|
176
|
+
|
|
177
|
+
### `get_booking_results(itinerary_or_token, **kwargs)`
|
|
178
|
+
|
|
179
|
+
Get fare options for a specific itinerary. Pass an `Itinerary` object directly, or a booking token string with explicit `origin`, `destination`, `date`, and `selected_legs`. Returns `list[BookingOption]` with `price`, `brand_label`, `brand_code`, `fare_family`, etc. `BookingOption` supports both attribute access (`opt.price`) and dict-style access (`opt["price"]`, `opt.get("price")`).
|
|
180
|
+
|
|
181
|
+
### Result types
|
|
182
|
+
|
|
183
|
+
- **`SearchResult`** — `best: list[Itinerary]`, `other: list[Itinerary]`, `price_range: PriceRange | None`
|
|
184
|
+
- **`Itinerary`** — Full itinerary with `price`, `flights`, `layovers`, `travel_time`, `booking_token`, `carbon_emissions`
|
|
185
|
+
- **`Flight`** — Segment details: `airline`, `flight_number`, `aircraft`, `legroom`, `co2_grams`, `amenities`
|
|
186
|
+
- **`Layover`** — Stop info: `minutes`, airports, `is_overnight`
|
|
187
|
+
- **`CarbonEmissions`** — `this_flight_grams`, `typical_for_route_grams`, `difference_percent`
|
|
188
|
+
|
|
189
|
+
### Constants
|
|
190
|
+
|
|
191
|
+
| Constant | Value | Description |
|
|
192
|
+
|----------|-------|-------------|
|
|
193
|
+
| `SORT_TOP` | `1` | Google's default ranking |
|
|
194
|
+
| `SORT_CHEAPEST` | `2` | Cheapest first |
|
|
195
|
+
| `SORT_DEPARTURE_TIME` | `3` | By departure time |
|
|
196
|
+
| `SORT_ARRIVAL_TIME` | `4` | By arrival time |
|
|
197
|
+
| `SORT_DURATION` | `5` | Shortest first |
|
|
198
|
+
|
|
199
|
+
## Pricing notes
|
|
200
|
+
|
|
201
|
+
Use `itinerary.price` to get the USD price as an integer.
|
|
202
|
+
|
|
203
|
+
By default, `search()` excludes basic economy fares so prices reflect Main Cabin. Pass `include_basic_economy=True` to include them:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
results = search("JFK", "LAX", "2026-06-15", include_basic_economy=True)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## How it works
|
|
210
|
+
|
|
211
|
+
Swoop uses Google Flights' internal `GetShoppingResults` and `GetBookingResults` RPC endpoints — the same ones the web app calls when you search for flights. Requests are serialized as nested JSON payloads and sent via HTTP POST with browser impersonation (via [primp](https://github.com/deedy5/primp)).
|
|
212
|
+
|
|
213
|
+
Responses are decoded from nested list structures into typed Python dataclasses.
|
|
214
|
+
|
|
215
|
+
## Dependencies
|
|
216
|
+
|
|
217
|
+
- **[primp](https://github.com/deedy5/primp)** — HTTP client with browser TLS impersonation
|
|
218
|
+
- **[protobuf](https://pypi.org/project/protobuf/)** — Protocol buffer serialization
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "swoop-flights"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Google Flights price scraper. Search flights programmatically via the same RPC endpoints the web app uses."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Ayush Saraswat" }]
|
|
13
|
+
keywords = ["google-flights", "flights", "travel", "scraper", "prices"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"primp>=1.0.0",
|
|
28
|
+
"protobuf>=4.0.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
validation = ["airportsdata>=1.3.0"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/saraswatayu/swoop"
|
|
36
|
+
Issues = "https://github.com/saraswatayu/swoop/issues"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.version]
|
|
39
|
+
path = "swoop/__init__.py"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["swoop"]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.sdist]
|
|
45
|
+
include = ["swoop/", "README.md", "LICENSE"]
|
|
46
|
+
|
|
47
|
+
[tool.pyright]
|
|
48
|
+
pythonVersion = "3.10"
|
|
49
|
+
typeCheckingMode = "basic"
|
|
50
|
+
exclude = ["swoop/flights_pb2.py", "tests/"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
markers = [
|
|
55
|
+
"live: integration tests that hit real Google Flights API (deselect with -m 'not live')",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.mutmut]
|
|
59
|
+
paths_to_mutate = "swoop/decoder.py,swoop/rpc.py,swoop/_validate.py,swoop/_booking.py"
|
|
60
|
+
tests_dir = "tests/"
|
|
61
|
+
runner = "python -m pytest tests/ -x --ignore=tests/test_booking_options_corpus.py --ignore=tests/test_booking_option_schema_hypotheses.py --ignore=tests/test_corpus_matrix_tools.py --ignore=tests/test_booking_option_field_explorer.py -m 'not live' -q"
|