gflights 0.2.1

Unofficial async Rust client for the Google Flights web API — search flights, price graphs, and booking offers.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# gflights

[![crates.io](https://img.shields.io/crates/v/gflights)](https://crates.io/crates/gflights)
[![docs.rs](https://img.shields.io/docsrs/gflights)](https://docs.rs/gflights)
[![CI](https://github.com/nas-/google-flights-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/nas-/google-flights-rs/actions/workflows/ci.yml)

Unofficial async Rust client for the [Google Flights](https://www.google.com/flights) web API.

Search flights, compare prices across a date range, retrieve booking offers, and resolve booking URLs — all without an official API key.

> **Disclaimer:** This library talks to the same endpoints used by the Google Flights website.  It is not affiliated with or endorsed by Google.  Usage is subject to Google's Terms of Service.

---

## Features

- **Flight search** — one-way, return, multi-stop itineraries
- **Price graph** — cheapest fares across a configurable date range
- **Date grid** — full departure × return price matrix for round trips
- **Booking offers** — airline/OTA offers with prices and booking URLs
- **City / airport lookup** — resolve city names and IATA codes
- **Multi-airport search** — up to 4 departure or destination airports
- **Airline / alliance filters** — include or exclude specific airlines or alliances (oneworld, SkyTeam, Star Alliance)
- **Connection filters** — require layover through specific airports; set min/max layover duration
- **Lower-emissions filter** — restrict to flights with below-average CO₂
- **Locale support**`language` + `country` for non-English results
- **Sort order** — Best · Price · Duration · Departure time · Arrival time
- **CO2 / emissions** — included in parsed itinerary data
- **Layover details** — connection time, airport codes, overnight warnings
- **Rate limiting** — built-in governor-based token-bucket limiter
- **Retry logic** — exponential back-off for transient 5xx / timeout errors
- **CLI** — interactive REPL and one-shot subcommands (`search`, `graph`, `dgrid`, `offer`)

---

## Installation

```toml
[dependencies]
gflights = "0.1.0"
tokio = { version = "1", features = ["full"] }
```

---

## CLI

The crate ships a `gflights` binary. Install it with:

```sh
cargo install gflights
```

### One-shot mode

```sh
# Search flights
gflights search --from LHR --to JFK --date 2026-08-01

# Round trip with filters
gflights search --from MXP --to NRT --date 2026-09-01 --return 2026-09-15 \
  --airline LX --airline ONEWORLD --via ZRH \
  --min-layover 60 --max-layover 180 \
  --lower-emissions --sort price --format json

# Multi-city (open-jaw) search
gflights mcity --leg LUX FCO 2026-09-10 --leg FCO MAD 2026-09-13 --leg MAD LUX 2026-09-17

# Price graph (cheapest fare per day over 3 months)
gflights graph --from LHR --to JFK --date 2026-08-01 --months 3

# Departure × return price grid
gflights dgrid --from LHR --to JFK \
  --dep-start 2026-08-01 --dep-end 2026-08-07 \
  --ret-start 2026-08-15 --ret-end 2026-08-22

# Booking offers with clickable URLs (OSC 8, supported in most modern terminals)
gflights offer --from FRA --to SIN --date 2026-10-01

# Explore cheap destinations (Google Flights Explore)
gflights explore --from LUX --month 9 --duration week --budget 150 --interest climbing

# Find cheapest departure dates (one-way)
gflights cheap --from LHR --to BCN --date 2026-08-01 --months 3

# Find cheapest round-trip combinations (fixed trip length)
gflights cheap --from LHR --to BCN --date 2026-08-01 --months 3 --trip-days 7

# Search with emissions column and layover detail
gflights search --from LUX --to SYD --date 2026-09-01 --show-co2 --detail
```

### Interactive REPL

Run `gflights` with no arguments to enter an interactive session with history:

```
gflights> search --from LHR --to JFK --date 2026-08-01
gflights> graph  --from MXP --to SYD --date 2026-09-01 --months 2
gflights> dgrid  --from LHR --to JFK --dep-start 2026-08-01 --dep-end 2026-08-07 --ret-start 2026-08-15 --ret-end 2026-08-22
gflights> quit
```

### `search` flag reference

| Flag | Default | Description |
|---|---|---|
| `--from <CODE>` | required | Departure airport IATA code or city name |
| `--to <CODE>` | required | Destination airport IATA code or city name |
| `--date <YYYY-MM-DD>` | required | Outbound departure date |
| `--return <YYYY-MM-DD>` | one-way | Return date |
| `--adults <N>` | `1` | Number of adult passengers |
| `--class <CLASS>` | `economy` | `economy` · `premium-economy` · `business` · `first` |
| `--stops <STOPS>` | `all` | `all` · `non-stop` · `one-stop` |
| `--sort <SORT>` | `best` | `best` · `price` · `duration` · `departure-time` · `arrival-time` ¹ |
| `--airline <CODE>` || Include airline IATA code or alliance (`ONEWORLD`, `SKYTEAM`, `STAR_ALLIANCE`). Repeatable. |
| `--exclude-airline <CODE>` || Exclude airline or alliance. Repeatable. |
| `--via <IATA>` || Require connection through this airport. Repeatable. |
| `--min-layover <MINS>` | none | Minimum layover in minutes (rounded up to 30 min intervals) |
| `--max-layover <MINS>` | none | Maximum layover in minutes |
| `--lower-emissions` | off | Restrict to below-average CO₂ flights |
| `--show-co2` | off | Add a CO₂ kg column to the table output |
| `--detail` | off | Show layover airports (`via ZRH (65 min)`) and `+1` for next-day arrivals |
| `--currency <CURRENCY>` | `euro` | Result currency (e.g. `us-dollar`, `british-pound`) |
| `--lang <CODE>` | `en` | BCP-47 language subtag |
| `--country <CODE>` | `GB` | ISO 3166-1 alpha-2 country code |
| `--format <FORMAT>` | `table` | `table` · `json` |

¹ `departure-time` and `arrival-time` are sorted client-side after Google returns results.

### `dgrid` flag reference

| Flag | Default | Description |
|---|---|---|
| `--from <CODE>` | required | Departure airport IATA code or city name |
| `--to <CODE>` | required | Destination airport IATA code or city name |
| `--dep-start <DATE>` | required | First outbound departure date |
| `--dep-end <DATE>` | required | Last outbound departure date |
| `--ret-start <DATE>` | required | First return date |
| `--ret-end <DATE>` | required | Last return date |
| `--adults <N>` | `1` | Number of adult passengers |
| `--class <CLASS>` | `economy` | Travel class |
| `--stops <STOPS>` | `all` | Stop filter |
| `--currency <CURRENCY>` | `euro` | Result currency |
| `--format <FORMAT>` | `table` | `table` · `json` |

---

## Quick start

### Search for flights

```rust
use gflights::requests::{api::ApiClient, config::Config};
use chrono::{Duration, Utc};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = ApiClient::new().await;
    let today = Utc::now().date_naive();

    let config = Config::builder()
        .departure("LHR", &client).await?   // London Heathrow
        .destination("JFK", &client).await? // New York JFK
        .departing_date(today + Duration::days(14))
        .return_date(today + Duration::days(21))
        .build()?;

    let results = client.request_flights(&config).await?;

    for resp in &results.responses {
        if let Some(flights) = resp.maybe_get_all_flights() {
            for f in &flights {
                println!(
                    "{} — {}h{}m — stops: {} — {:?}",
                    f.itinerary.flight_by,
                    f.itinerary.total_time_minutes / 60,
                    f.itinerary.total_time_minutes % 60,
                    f.itinerary.stop_count(),
                    f.itinerary_cost.trip_cost,
                );
            }
        }
    }
    Ok(())
}
```

Run the full worked example:

```sh
cargo run --example flights
```

### Price graph across a date range

```rust
use gflights::requests::{api::ApiClient, config::{Config, Currency}};
use chrono::{Duration, Months, Utc};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = ApiClient::new().await;
    let today = Utc::now().date_naive();

    let config = Config::builder()
        .departure("MAD", &client).await?
        .destination("MEX", &client).await?
        .departing_date(today + Duration::days(10))
        .currency(Currency::USDollar)
        .build()?;

    let graph = client.request_graph(&config, Months::new(3)).await?;

    if let Some((date, price)) = graph
        .get_all_graphs()
        .iter()
        .filter_map(|g| g.maybe_get_date_price())
        .min_by_key(|&(_, p)| p)
    {
        println!("Cheapest: {} at ${:.2}", date, price);
    }
    Ok(())
}
```

Run with:

```sh
cargo run --example graph
```

### Multi-city (open-jaw) search

```rust
use gflights::requests::{api::ApiClient, config::MultiCityConfig};
use chrono::NaiveDate;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = ApiClient::new().await;

    let config = MultiCityConfig::builder()
        .add_leg("LUX", "FCO", NaiveDate::from_ymd_opt(2026, 9, 10).unwrap(), &client).await?
        .add_leg("FCO", "MAD", NaiveDate::from_ymd_opt(2026, 9, 13).unwrap(), &client).await?
        .add_leg("MAD", "LUX", NaiveDate::from_ymd_opt(2026, 9, 17).unwrap(), &client).await?
        .build()?;

    let results = client.request_multi_city_flights(&config).await?;
    let flights = results.get_all_flights();
    println!("Found {} flight options across {} legs", flights.len(), config.legs.len());
    Ok(())
}
```

Run with:

```sh
cargo run --example multi_city
```

---

## Python bindings

The `gflights-py/` directory provides async Python bindings built with [pyo3](https://pyo3.rs) and [maturin](https://www.maturin.rs).

### Install

```sh
pip install gflights          # when published to PyPI
# or build from source:
cd gflights-py && maturin develop
```

### Quick start

```python
import asyncio
import gflights

async def main():
    client = gflights.GFlights()

    # One-way search
    flights = await client.search(
        from_airport="LHR", to_airport="JFK", date="2026-08-01",
    )
    for f in flights:
        print(f.airline, f.duration_minutes, f.price)

    # Price graph — cheapest fare per day over 3 months
    graph = await client.price_graph(
        from_airport="LHR", to_airport="JFK", date="2026-08-01", months=3
    )
    cheapest = min(graph, key=lambda e: e.price)
    print(cheapest.date, cheapest.price)

    # Departure × return price grid
    grid = await client.date_grid(
        from_airport="LHR", to_airport="JFK",
        dep_start="2026-08-01", dep_end="2026-08-07",
        ret_start="2026-08-14", ret_end="2026-08-21",
    )
    best = min(grid, key=lambda e: e.price)
    print(best.dep_date, "→", best.ret_date, best.price)

    # Cheapest departure dates (one-way)
    dates = await client.cheapest_dates(
        from_airport="LHR", to_airport="JFK", date="2026-08-01", months=3
    )
    for d in dates[:5]:
        print(d.departure_date, d.price)

    # Cheapest round-trip combinations (7-night stay)
    rt_dates = await client.cheapest_dates(
        from_airport="LHR", to_airport="JFK",
        date="2026-08-01", months=3, trip_duration_days=7
    )
    for d in rt_dates[:5]:
        print(d.departure_date, "→", d.return_date, d.price)

    # Explore cheap destinations
    dests = await client.explore(
        from_airport="LUX", month=9, duration="week",
        max_price=200, interest="beaches",
    )
    for d in sorted(dests, key=lambda x: x.price or 9999)[:5]:
        print(d.name, d.country, d.flight_airport or d.nearest_airport, d.price)

    # Run multiple searches concurrently
    lhr_jfk, mad_mex = await asyncio.gather(
        client.search(from_airport="LHR", to_airport="JFK", date="2026-09-01"),
        client.search(from_airport="MAD", to_airport="MEX", date="2026-09-01"),
    )

asyncio.run(main())
```

### Error handling

All API errors raise `gflights.GFlightsError` (a subclass of `Exception`).
Input validation errors (bad date, unknown currency, etc.) raise `ValueError`.

```python
try:
    flights = await client.search(from_airport="LHR", to_airport="JFK", date="2026-08-01")
except gflights.GFlightsError as e:
    print(f"API error: {e}")
except ValueError as e:
    print(f"Bad input: {e}")
```

### Rate limiting

The `client.rate_limited` flag is set to `True` when Google returns HTTP 429.
Call `client.reset_rate_limit()` after a cooling-off period.

### Python type stubs

Full `.pyi` stubs are shipped with the package.  Every method and class is typed and documented, so IDE auto-completion and mypy work out of the box.

---

## Configuration reference

| Builder method | Type | Default | Description |
|---|---|---|---|
| `.departure(iata, &client)` | `async &str` | required | Departure airport / city |
| `.destination(iata, &client)` | `async &str` | required | Destination airport / city |
| `.departure_location(loc)` | `Location` || Set departure from existing `Location` (no network) |
| `.destination_location(loc)` | `Location` || Set destination from existing `Location` |
| `.add_departure(iata, &client)` | `async &str` || Add extra departure (max 4) |
| `.add_destination(iata, &client)` | `async &str` || Add extra destination (max 4) |
| `.departing_date(date)` | `NaiveDate` | required | Outbound departure date |
| `.return_date(date)` | `NaiveDate` | one-way | Return date (omit for one-way) |
| `.travelers(t)` | `Travelers` | 1 adult | Passenger counts |
| `.travel_class(c)` | `TravelClass` | Economy | Economy / Business / First |
| `.stop_options(s)` | `StopOptions` | Any | Non-stop · Max1 · Any |
| `.sort_order(s)` | `SortOrder` | Best | Best · Price · Duration · DepartureTime · ArrivalTime |
| `.currency(c)` | `Currency` | EUR | Result currency |
| `.language(s)` | `&str` | `"en"` | BCP-47 language subtag |
| `.country(s)` | `&str` | `"GB"` | ISO 3166-1 alpha-2 country code |
| `.stopover_min(d)` | `StopoverDuration` | Unlimited | Minimum layover duration |
| `.stopover_max(d)` | `StopoverDuration` | Unlimited | Maximum layover duration |
| `.duration_max(d)` | `TotalDuration` | Unlimited | Maximum total trip duration |
| `.departing_times(t)` | `FlightTimes` | Any | Outbound departure time window |
| `.return_times(t)` | `FlightTimes` | Any | Return departure time window |
| `.airlines_include(v)` | `Vec<AirlineFilter>` | none | Restrict to these airlines / alliances |
| `.add_airline_include(f)` | `AirlineFilter` || Add one airline / alliance to include filter |
| `.airlines_exclude(v)` | `Vec<AirlineFilter>` | none | Exclude these airlines / alliances |
| `.add_airline_exclude(f)` | `AirlineFilter` || Add one airline / alliance to exclude filter |
| `.connecting_airports(v)` | `Vec<String>` | none | Require connection through these IATA airport codes |
| `.add_connecting_airport(s)` | `&str` || Add one connecting airport |
| `.lower_emissions(b)` | `bool` | `false` | Restrict to below-average CO₂ flights |

### Travelers

```rust
use gflights::parsers::common::Travelers;

// [adults, children, infants_in_seat, infants_on_lap]
let travelers = Travelers::new(vec![2, 1, 0, 0])?; // 2 adults + 1 child
```

Rules: at least 1 adult, total ≤ 9 passengers.

### Airline & connection filters

```rust
use gflights::parsers::common::{AirlineFilter, Alliance};

let config = Config::builder()
    .departure("LHR", &client).await?
    .destination("JFK", &client).await?
    .departing_date(date)
    // Only show British Airways and oneworld alliance members
    .add_airline_include("BA".parse::<AirlineFilter>()?)
    .add_airline_include(AirlineFilter::Alliance(Alliance::OneWorld))
    // Must connect through Dublin
    .add_connecting_airport("DUB")
    // At least 45 min layover, at most 3 hours
    .stopover_min(StopoverDuration::Minutes(45))
    .stopover_max(StopoverDuration::Minutes(180))
    // Lower CO₂ only
    .lower_emissions(true)
    .build()?;
```

---

## Rate limiting

`ApiClient` uses a [governor](https://crates.io/crates/governor) token-bucket rate limiter (default: 10 req/s).

```rust
use gflights::requests::api::ApiClient;
use governor::Quota;
use std::num::NonZeroU32;

// Custom: 2 requests per second
let quota = Quota::per_second(NonZeroU32::new(2).unwrap());
let client = ApiClient::new_with_ratelimit(quota).await;
```

If Google returns HTTP 429, the client sets an internal flag and all subsequent requests immediately return `RateLimitedError` without touching the network.  Reset it after a cooling-off period:

```rust
if client.is_rate_limited() {
    tokio::time::sleep(std::time::Duration::from_secs(60)).await;
    client.reset_rate_limit();
}
```

---

## Retry logic

Transient server errors (HTTP 500/502/503/504) and connection timeouts are automatically retried with exponential back-off.  Defaults: 3 attempts, 500 ms base delay, 30 s cap.

```rust
use gflights::{requests::api::ApiClient, RetryConfig};

let client = ApiClient::new().await
    .with_retry_config(RetryConfig {
        max_attempts: 5,
        base_delay_ms: 200,
        cap_delay_ms: 10_000,
    });
```

Set `max_attempts: 1` to disable retries entirely.

---

## Error handling

All public async methods return `anyhow::Result<T>`.  Downcast `RateLimitedError` to check for 429:

```rust
use gflights::RateLimitedError;

match client.request_flights(&config).await {
    Ok(resp) => { /* use resp */ }
    Err(e) if e.downcast_ref::<RateLimitedError>().is_some() => {
        eprintln!("Rate limited — back off and retry");
    }
    Err(e) => eprintln!("Other error: {e}"),
}
```

---

## Known limitations

`x-goog-batchexecute-bgr` header — computed deep in Google's obfuscated JS from the current time and request payload length — is omitted.  Responses are still valid but may occasionally be less accurate (e.g. missing low-fare calendar data).  Contributions to reverse-engineer the algorithm are welcome.

---

## Development

```sh
# Build
cargo build

# Unit tests (no network)
cargo test --lib

# Binary (CLI) tests
cargo test --bin gflights

# Doc tests
cargo test --doc

# Live integration tests (requires internet, skipped in CI)
RUN_LIVE_TESTS=1 cargo test --lib -- --ignored

# Docs
cargo doc --open

# Lint
cargo clippy --all-targets -- -D warnings

# Format
cargo fmt

# Security audit
cargo audit
```

---

## Contributing

Install the pre-commit hook once per clone:
```sh
git config core.hooksPath hooks
chmod +x hooks/pre-commit  # Unix/macOS only
```
The hook runs `cargo fmt`, `cargo clippy`, `rustdoc`, `cargo test --lib`, and offline pytest
before every commit. Requires `maturin develop` to have been run at least once for the Python check.