gflights 0.3.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
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
# 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
- **Flight deals** — discounted destinations from an origin (price vs typical, discount %, booking link)
- **City / airport lookup** — resolve city names and IATA codes
- **Multi-airport search** — up to 7 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

# Find discounted destinations (Google Flights deals)
gflights deals --from LUX --out 2026-06-20 --ret 2026-06-24 --nonstop

# 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` |

### Global flags (any subcommand)

| Flag | Default | Description |
|---|---|---|
| `--proxy <URL>` | none | Route all requests through a proxy. Supports `http://`, `https://`, `socks5://` (e.g. `socks5://127.0.0.1:9050`). |
| `--user-agent <UA>` | random | Override the User-Agent. By default a real desktop browser string is chosen from a rotating pool per run. |

```sh
# Search through a local SOCKS5 proxy
gflights --proxy socks5://127.0.0.1:9050 search --from LHR --to JFK --date 2026-09-15
```

---

## Proxy & Docker sidecar

Every request — including the one-time frontend-version probe — is routed
through the configured proxy:

```rust
let client = ApiClient::new_with_proxy("socks5://127.0.0.1:9050").await?;
```

```python
from gflights import Client
client = Client(proxy="socks5://127.0.0.1:9050")
```

For rotating egress IPs, run a proxy as a sidecar container and share its
network namespace. The included [`docker-compose.yml`](docker-compose.yml) shows
the pattern — `gflights` runs with `network_mode: "service:proxy"`, so all of
its traffic leaves through the `proxy` container:

```sh
docker compose run --rm gflights search --from LHR --to JFK --date 2026-09-15
```

Swap the `proxy` service for any HTTP/SOCKS5 proxy (or a rotating-IP service)
without touching the `gflights` service.

---

## MCP server

Run gflights as a [Model Context Protocol](https://modelcontextprotocol.io)
server over stdio, exposing flight tools to MCP clients such as Claude Desktop:

```sh
gflights mcp
```

It speaks JSON-RPC 2.0 on stdin/stdout and exposes these tools: `search`,
`price_graph`, `cheapest_dates`, `explore`, `deals`. Each maps its JSON arguments
to the same library calls the CLI uses and returns the result as JSON.

Example client configuration (Claude Desktop `claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "gflights": {
      "command": "gflights",
      "args": ["mcp"]
    }
  }
}
```

The global `--proxy` and `--user-agent` flags work here too, e.g.
`"args": ["--proxy", "socks5://127.0.0.1:9050", "mcp"]`.

---

## 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

Route arguments are `origin` / `destination` (each takes an IATA code **or** a
city name). Passenger counts are grouped into a `Passengers` object and the
shared result filters into a `SearchFilters` object — pass only what you need.

```python
import asyncio
from gflights import Client, Passengers, SearchFilters

async def main():
    client = Client()

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

    # Two adults + a child, non-stop only, sorted by price
    flights = await client.search(
        origin="LHR", destination="JFK", date="2026-08-01",
        passengers=Passengers(adults=2, children=1),
        filters=SearchFilters(stops="nonstop", sort="price"),
    )

    # Price graph — cheapest fare per day over 3 months
    graph = await client.price_graph(
        origin="LHR", destination="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(
        origin="LHR", destination="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(
        origin="LHR", destination="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(
        origin="LHR", destination="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(
        origin="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(origin="LHR", destination="JFK", date="2026-09-01"),
        client.search(origin="MAD", destination="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(origin="LHR", destination="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.