find-flight 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/examples/search_flights.py +136 -0
- package/extensions/flight-search.ts +174 -0
- package/flight_runner.py +153 -0
- package/package.json +48 -0
- package/skills/find-flight/SKILL.md +205 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 find-flight contributors
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# find-flight
|
|
2
|
+
|
|
3
|
+
**Google Flights search for AI agents.** A [pi](https://github.com/earendil-works/pi) package that bundles:
|
|
4
|
+
|
|
5
|
+
- **a `search_flights` extension tool** — deterministic, structured flight searches the agent calls directly
|
|
6
|
+
- **a portable skill** — recipes and reference that work in pi, Claude Code, and Codex
|
|
7
|
+
|
|
8
|
+
Powered by the [`fast-flights`](https://github.com/AWeirdDev/flights) Google Flights scraper. Read-only — it fetches real availability and prices, but does **not** book anything.
|
|
9
|
+
|
|
10
|
+
## ✨ What it does
|
|
11
|
+
|
|
12
|
+
- Search flights between airports on given dates (one-way or round-trip)
|
|
13
|
+
- Real prices, airlines, per-leg times, duration, plane type, stop counts, carbon emissions
|
|
14
|
+
- Filters: seat class, passengers, currency, language, max stops, max price, exclude basic economy
|
|
15
|
+
- Returns **structured** results — correct per-direction stop counts, no ad-hoc scripts left behind
|
|
16
|
+
|
|
17
|
+
## 📦 Install
|
|
18
|
+
|
|
19
|
+
Requires **Python 3.10+** with fast-flights, and live network access:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install fast-flights
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### As a pi package (<img src="https://img.shields.io/badge/pi-package-5B8DEF)" width="60px" align="center">)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pi install git:github.com/<your-user>/find-flight
|
|
29
|
+
# or from npm if published:
|
|
30
|
+
pi install npm:find-flight
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install only for the current run:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pi -e git:github.com/<your-user>/find-flight
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
After install, reload pi (`/reload`) — the agent gains a `search_flights` tool automatically.
|
|
40
|
+
|
|
41
|
+
### Manually (local dev / other harnesses)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# pi extension
|
|
45
|
+
ln -s "$(pwd)/extensions/flight-search.ts" ~/.pi/agent/extensions/flight-search.ts
|
|
46
|
+
|
|
47
|
+
# skill (pi)
|
|
48
|
+
ln -s "$(pwd)/skills/find-flight" ~/.pi/agent/skills/find-flight
|
|
49
|
+
# skill (Claude Code / Codex)
|
|
50
|
+
ln -s "$(pwd)/skills/find-flight" ~/.agents/skills/find-flight
|
|
51
|
+
ln -s "$(pwd)/skills/find-flight" ~/.claude/skills/find-flight
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 🚀 Usage
|
|
55
|
+
|
|
56
|
+
Just ask in natural language:
|
|
57
|
+
|
|
58
|
+
> *"Find one-way flights from IST to ECN on 12 September, economy, show cheapest 3"*
|
|
59
|
+
>
|
|
60
|
+
> *"Cheapest round-trip Istanbul to Paris, out 23 Oct return 25 Oct"*
|
|
61
|
+
|
|
62
|
+
The agent calls `search_flights(from_airport, to_airport, outbound_date, trip, return_date?, seat, currency, ...)` and gets structured JSON.
|
|
63
|
+
|
|
64
|
+
### Python executor (standalone)
|
|
65
|
+
|
|
66
|
+
`flight_runner.py` reads a JSON query on stdin and writes structured JSON on stdout — useful for testing or scripting without the agent:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
echo '{"from_airport":"IST","to_airport":"ECN","outbound_date":"2026-09-12","trip":"one-way","currency":"EUR"}' \
|
|
70
|
+
| python3 flight_runner.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### CLI example
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python examples/search_flights.py --from IST --to ECN --date 2026-09-12 --currency EUR --sort price --limit 3
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 📁 Layout
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
find-flight/
|
|
83
|
+
├── package.json # pi package manifest (extensions + skills)
|
|
84
|
+
├── extensions/
|
|
85
|
+
│ └── flight-search.ts # registerTool: search_flights
|
|
86
|
+
├── skills/
|
|
87
|
+
│ └── find-flight/SKILL.md # portable agent skill
|
|
88
|
+
├── flight_runner.py # Python executor (structured JSON)
|
|
89
|
+
├── examples/
|
|
90
|
+
│ └── search_flights.py # CLI example
|
|
91
|
+
└── README.md
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## ⚙️ Notes / limitations
|
|
95
|
+
|
|
96
|
+
- **Round-trip data (verified):** parsed results contain only the **outbound** legs; `price` is the **two-way total**. The tool reports this explicitly.
|
|
97
|
+
- `multi-city` trips are not supported by the upstream library.
|
|
98
|
+
- Scraping Google is subject to rate limits / IP blocks. For heavy use, plug a proxy (BrightData) or richer source (SearchApi) integration into `fast-flights`.
|
|
99
|
+
- Prices are quoted in the `currency` you pass; if omitted, Google picks a default (often the local currency).
|
|
100
|
+
- Config: point the runner elsewhere with `FLIGHT_RUNNER=/path/to/flight_runner.py`, or choose a different interpreter with `FLIGHT_PYTHON=...`.
|
|
101
|
+
|
|
102
|
+
## 🔒 Security
|
|
103
|
+
|
|
104
|
+
This package executes Python (`flight_runner.py`) that makes live network requests to Google. Review the source before installing.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Search Google Flights with fast-flights from the command line.
|
|
3
|
+
|
|
4
|
+
Requires: pip install fast-flights
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
python search_flights.py --from TPE --to MYJ --date 2025-01-01
|
|
8
|
+
python search_flights.py --from TPE --to MYJ --date 2025-01-01 --trip round-trip --return-date 2025-01-08 --seat business
|
|
9
|
+
python search_flights.py --from TPE --to MYJ --date 2025-01-01 --max-stops 1 --max-price 500 --currency USD --sort price --limit 5
|
|
10
|
+
echo "Flights from TPE to MYJ on 2025-01-01 one way economy" | python search_flights.py --text
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import date as Date
|
|
17
|
+
|
|
18
|
+
from fast_flights import (
|
|
19
|
+
FlightsNotFound,
|
|
20
|
+
FlightQuery,
|
|
21
|
+
Passengers,
|
|
22
|
+
ResultList,
|
|
23
|
+
create_query,
|
|
24
|
+
get_flights,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
TRIPS = ("one-way", "round-trip")
|
|
28
|
+
SEATS = ("economy", "premium-economy", "business", "first")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
32
|
+
p = argparse.ArgumentParser(description="Search Google Flights with fast-flights.")
|
|
33
|
+
p.add_argument("--text", action="store_true",
|
|
34
|
+
help="Read a natural-language query from stdin instead of structured flags.")
|
|
35
|
+
p.add_argument("--from", dest="from_airport", required=False,
|
|
36
|
+
help="3-letter IATA departure airport.")
|
|
37
|
+
p.add_argument("--to", dest="to_airport", required=False,
|
|
38
|
+
help="3-letter IATA arrival airport.")
|
|
39
|
+
p.add_argument("--date", type=Date.fromisoformat,
|
|
40
|
+
help="Departure date, YYYY-MM-DD.")
|
|
41
|
+
p.add_argument("--return-date", type=Date.fromisoformat,
|
|
42
|
+
help="Return date for round-trip, YYYY-MM-DD.")
|
|
43
|
+
p.add_argument("--trip", choices=TRIPS, default="one-way")
|
|
44
|
+
p.add_argument("--seat", choices=SEATS, default="economy")
|
|
45
|
+
p.add_argument("--adults", type=int, default=1)
|
|
46
|
+
p.add_argument("--children", type=int, default=0)
|
|
47
|
+
p.add_argument("--currency", default="")
|
|
48
|
+
p.add_argument("--language", default="")
|
|
49
|
+
p.add_argument("--max-stops", type=int)
|
|
50
|
+
p.add_argument("--max-price", type=int)
|
|
51
|
+
p.add_argument("--sort", choices=("price", "duration"), default=None,
|
|
52
|
+
help="Sort results by price (ascending) or total duration (ascending).")
|
|
53
|
+
p.add_argument("--limit", type=int, default=0,
|
|
54
|
+
help="Show only the top N itineraries after sorting (0 = all).")
|
|
55
|
+
return p
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def leg(date: Date, src: str, dst: str, max_stops: int | None) -> FlightQuery:
|
|
59
|
+
src, dst = src.strip().upper(), dst.strip().upper()
|
|
60
|
+
for code in (src, dst):
|
|
61
|
+
if not code.isalpha() or len(code) != 3:
|
|
62
|
+
sys.exit(f"airport codes must be 3 letters, got {code!r}")
|
|
63
|
+
return FlightQuery(
|
|
64
|
+
date=date.strftime("%Y-%m-%d"),
|
|
65
|
+
from_airport=src,
|
|
66
|
+
to_airport=dst,
|
|
67
|
+
max_stops=max_stops,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_query(args: argparse.Namespace):
|
|
72
|
+
flights = [leg(args.date, args.from_airport, args.to_airport, args.max_stops)]
|
|
73
|
+
if args.trip == "round-trip":
|
|
74
|
+
if not args.return_date:
|
|
75
|
+
sys.exit("--return-date is required for --trip round-trip")
|
|
76
|
+
flights.append(leg(args.return_date, args.to_airport, args.from_airport, args.max_stops))
|
|
77
|
+
|
|
78
|
+
return create_query(
|
|
79
|
+
flights=flights,
|
|
80
|
+
trip=args.trip,
|
|
81
|
+
seat=args.seat,
|
|
82
|
+
passengers=Passengers(adults=args.adults, children=args.children),
|
|
83
|
+
currency=args.currency,
|
|
84
|
+
language=args.language,
|
|
85
|
+
max_price=args.max_price,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def total_duration(flight) -> int:
|
|
90
|
+
return sum(seg.duration for seg in flight.flights)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def print_result(result: ResultList, currency: str = "") -> None:
|
|
94
|
+
if not result:
|
|
95
|
+
print("No flights found.")
|
|
96
|
+
return
|
|
97
|
+
sym = f"{currency} " if currency else "$"
|
|
98
|
+
for flight in result:
|
|
99
|
+
print(f"{sym}{flight.price} {', '.join(flight.airlines)} ({flight.type})")
|
|
100
|
+
for seg in flight.flights:
|
|
101
|
+
dep = f"{seg.departure.date[0]}-{seg.departure.date[1]:02d}-{seg.departure.date[2]:02d} {seg.departure.time[0]:02d}:{seg.departure.time[1]:02d}"
|
|
102
|
+
arr = f"{seg.arrival.date[0]}-{seg.arrival.date[1]:02d}-{seg.arrival.date[2]:02d} {seg.arrival.time[0]:02d}:{seg.arrival.time[1]:02d}"
|
|
103
|
+
print(f" {seg.from_airport.code} {dep} -> {seg.to_airport.code} {arr} "
|
|
104
|
+
f"({seg.duration} min, {seg.plane_type})")
|
|
105
|
+
print(f"\n{len(result)} itinerary/ies")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main() -> int:
|
|
109
|
+
args = build_parser().parse_args()
|
|
110
|
+
|
|
111
|
+
if args.text:
|
|
112
|
+
query = sys.stdin.read().strip()
|
|
113
|
+
if not query:
|
|
114
|
+
sys.exit("no text on stdin")
|
|
115
|
+
result = get_flights(query)
|
|
116
|
+
else:
|
|
117
|
+
query = build_query(args)
|
|
118
|
+
result = get_flights(query)
|
|
119
|
+
|
|
120
|
+
if args.sort == "price":
|
|
121
|
+
result.sort(key=lambda f: f.price)
|
|
122
|
+
elif args.sort == "duration":
|
|
123
|
+
result.sort(key=total_duration)
|
|
124
|
+
if args.limit:
|
|
125
|
+
result = result[: args.limit]
|
|
126
|
+
|
|
127
|
+
print_result(result, currency=args.currency)
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
try:
|
|
133
|
+
raise SystemExit(main())
|
|
134
|
+
except FlightsNotFound:
|
|
135
|
+
print("No flights found (Google returned an error/no results).")
|
|
136
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flight Search — a pi extension that registers a `search_flights` tool.
|
|
3
|
+
*
|
|
4
|
+
* The tool delegates to `flight_runner.py` (a small Python wrapper around the
|
|
5
|
+
* `fast-flights` Google Flights scraper) for deterministic, structured results:
|
|
6
|
+
* correct per-direction stop counts, no scratch scripts, no ad-hoc formatting.
|
|
7
|
+
*
|
|
8
|
+
* Install:
|
|
9
|
+
* - python3 with `fast-flights` installed (the runner reports a clear error if not)
|
|
10
|
+
* - symlink this file into your pi extensions dir so it auto-loads on /reload:
|
|
11
|
+
* ln -s "$(pwd)/extensions/flight-search.ts" ~/.pi/agent/extensions/flight-search.ts
|
|
12
|
+
*
|
|
13
|
+
* The runner path is resolved in this order:
|
|
14
|
+
* 1. `FLIGHT_RUNNER` env var
|
|
15
|
+
* 2. `flight_runner.py` next to this file
|
|
16
|
+
* 3. default OSS folder: <repo>/flight_runner.py
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFile } from "node:child_process";
|
|
20
|
+
import { accessSync, realpathSync } from "node:fs";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
|
|
23
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
24
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import { Type } from "typebox";
|
|
26
|
+
|
|
27
|
+
export function findRunner(): string {
|
|
28
|
+
const env = process.env.FLIGHT_RUNNER;
|
|
29
|
+
if (env) return env;
|
|
30
|
+
|
|
31
|
+
// __dirname is the dir the extension was LOADED from. If the extension is
|
|
32
|
+
// symlinked into ~/.pi/agent/extensions/, __dirname is that symlink dir, so
|
|
33
|
+
// resolve the real path of this module before looking for its neighbours.
|
|
34
|
+
let base = __dirname;
|
|
35
|
+
try {
|
|
36
|
+
base = dirname(realpathSync(__filename));
|
|
37
|
+
} catch {
|
|
38
|
+
/* fall back to __dirname */
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 1) flight_runner.py next to this file
|
|
42
|
+
const local = join(base, "flight_runner.py");
|
|
43
|
+
if (fileExists(local)) return local;
|
|
44
|
+
// 2) OSS project layout: <project>/flight_runner.py
|
|
45
|
+
const project = join(dirname(base), "flight_runner.py");
|
|
46
|
+
if (fileExists(project)) return project;
|
|
47
|
+
// 3) last resort: current working dir
|
|
48
|
+
return join(process.cwd(), "flight_runner.py");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fileExists(p: string): boolean {
|
|
52
|
+
try {
|
|
53
|
+
accessSync(p);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runPython(runner: string, python: string, payload: unknown, signal?: AbortSignal): Promise<string> {
|
|
61
|
+
return new Promise((resolvePromise, reject) => {
|
|
62
|
+
const child = execFile(
|
|
63
|
+
python,
|
|
64
|
+
[runner],
|
|
65
|
+
{
|
|
66
|
+
timeout: 120_000,
|
|
67
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
68
|
+
signal,
|
|
69
|
+
},
|
|
70
|
+
(error, stdout, stderr) => {
|
|
71
|
+
if (error) {
|
|
72
|
+
reject(
|
|
73
|
+
new Error(
|
|
74
|
+
`flight search failed (${error.message})${stderr ? `: ${stderr.trim()}` : ""}`,
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
resolvePromise(stdout);
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
child.stdin?.on("error", () => {}); // EPIPE if python exits early
|
|
83
|
+
child.stdin?.end(JSON.stringify(payload));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const search_flights = {
|
|
88
|
+
name: "search_flights",
|
|
89
|
+
label: "Search Flights",
|
|
90
|
+
description:
|
|
91
|
+
"Search real Google Flights availability between two airports on given dates via fast-flights. Returns structured JSON: price (currency), airline(s), number of stops, and each flight leg with airports, times, duration and plane type. For round-trips, price is the two-way total and only the outbound legs are returned. Use for finding flights, checking prices/availability, comparing routes, dates, seats, or airlines.",
|
|
92
|
+
promptSnippet: "Search live Google Flights for price/availability between airports",
|
|
93
|
+
promptGuidelines: [
|
|
94
|
+
"Use search_flights for real flight prices and availability instead of guessing or writing ad-hoc scraping scripts.",
|
|
95
|
+
"Before calling search_flights, verify airport codes (from_airport/to_airport must be 3-letter IATA codes) and resolve dates to YYYY-MM-DD; ask the user if the code, trip type, or dates are unclear.",
|
|
96
|
+
"search_flights prices carry the currency you pass; pass a currency (e.g. 'EUR', 'TRY', 'USD') unless the user prefers Google's default.",
|
|
97
|
+
],
|
|
98
|
+
parameters: Type.Object({
|
|
99
|
+
from_airport: Type.String({ description: "3-letter IATA departure airport code (e.g. 'IST')" }),
|
|
100
|
+
to_airport: Type.String({ description: "3-letter IATA arrival airport code (e.g. 'ECN')" }),
|
|
101
|
+
outbound_date: Type.String({ description: "Outbound departure date, YYYY-MM-DD" }),
|
|
102
|
+
trip: StringEnum(["one-way", "round-trip"] as const),
|
|
103
|
+
return_date: Type.Optional(
|
|
104
|
+
Type.String({ description: "Return date, YYYY-MM-DD — required when trip is round-trip" }),
|
|
105
|
+
),
|
|
106
|
+
seat: StringEnum(["economy", "premium-economy", "business", "first"] as const),
|
|
107
|
+
currency: Type.Optional(
|
|
108
|
+
Type.String({ description: "ISO currency code, e.g. EUR, USD, TRY. Omit for Google default." }),
|
|
109
|
+
),
|
|
110
|
+
language: Type.Optional(
|
|
111
|
+
Type.String({ description: "IETF language tag, e.g. en-US. Omit for Google default." }),
|
|
112
|
+
),
|
|
113
|
+
adults: Type.Optional(Type.Integer({ minimum: 0, maximum: 9 })),
|
|
114
|
+
children: Type.Optional(Type.Integer({ minimum: 0, maximum: 9 })),
|
|
115
|
+
max_stops: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
116
|
+
max_price: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
117
|
+
exclude_basic_economy: Type.Optional(Type.Boolean()),
|
|
118
|
+
}),
|
|
119
|
+
|
|
120
|
+
async execute(_toolCallId, params, signal, onUpdate, _ctx) {
|
|
121
|
+
onUpdate?.({ content: [{ type: "text", text: "Searching flights…" }] });
|
|
122
|
+
|
|
123
|
+
if (params.trip === "round-trip" && !params.return_date) {
|
|
124
|
+
throw new Error("return_date is required when trip is 'round-trip'");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const runner = findRunner();
|
|
128
|
+
const python = process.env.FLIGHT_PYTHON ?? "python3";
|
|
129
|
+
const stdout = await runPython(runner, python, params, signal);
|
|
130
|
+
|
|
131
|
+
let data: unknown;
|
|
132
|
+
try {
|
|
133
|
+
data = JSON.parse(stdout);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(`flight search returned invalid JSON: ${stdout.slice(0, 300)}`);
|
|
136
|
+
}
|
|
137
|
+
const obj = data as Record<string, unknown>;
|
|
138
|
+
if (obj.error) throw new Error(`flight search error: ${obj.error}`);
|
|
139
|
+
|
|
140
|
+
const flights = (obj.flights ?? []) as Array<Record<string, unknown>>;
|
|
141
|
+
if (flights.length === 0) {
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: "No flights found for this search." }],
|
|
144
|
+
details: { currency: obj.currency, count: 0 },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
flights.sort((a, b) => Number(a.price) - Number(b.price));
|
|
148
|
+
const top = flights.slice(0, 10);
|
|
149
|
+
|
|
150
|
+
const lines = top.map((f, i) => {
|
|
151
|
+
const legs = (f.legs as Array<Record<string, string>>) ?? [];
|
|
152
|
+
const seg = legs.map(
|
|
153
|
+
(l) =>
|
|
154
|
+
`${l.from} ${l.departure} → ${l.to} ${l.arrival} ${l.date} (${Math.round(
|
|
155
|
+
(Number(l.duration_minutes) || 0) / 60,
|
|
156
|
+
)}h, ${l.plane})`,
|
|
157
|
+
).join(", ");
|
|
158
|
+
const rt = f.is_round_trip ? " (round-trip total)" : "";
|
|
159
|
+
const airline = (f.airlines as string[] | undefined)?.join("/") ?? "";
|
|
160
|
+
const stops = Number(f.stops) || 0;
|
|
161
|
+
return `${i + 1}. ${f.currency} ${f.price}${rt} · ${airline} · ${stops === 0 ? "direct" : `${stops} stop`}\n ${seg}`;
|
|
162
|
+
});
|
|
163
|
+
const summary = `${lines.join("\n")}\n\n${flights.length} itinerary/ies (showing cheapest ${top.length})`;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: summary }],
|
|
167
|
+
details: { currency: obj.currency, flights },
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export default function (pi: ExtensionAPI) {
|
|
173
|
+
pi.registerTool(search_flights);
|
|
174
|
+
}
|
package/flight_runner.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Search Google Flights via fast-flights and emit structured JSON.
|
|
3
|
+
|
|
4
|
+
This is the deterministic executor behind the pi `search_flights` extension
|
|
5
|
+
tool (and usable standalone). It reads a JSON query object on stdin and writes
|
|
6
|
+
a JSON result object on stdout.
|
|
7
|
+
|
|
8
|
+
Query keys (all optional unless noted):
|
|
9
|
+
from_airport (required), to_airport (required)
|
|
10
|
+
outbound_date (required): "YYYY-MM-DD"
|
|
11
|
+
return_date: "YYYY-MM-DD" for round-trip
|
|
12
|
+
trip: "one-way" | "round-trip"
|
|
13
|
+
seat: economy | premium-economy | business | first
|
|
14
|
+
adults, children, infants_in_seat, infants_on_lap: int
|
|
15
|
+
currency: ISO code or "" (Google default)
|
|
16
|
+
language: IETF tag or ""
|
|
17
|
+
max_stops, max_price: int
|
|
18
|
+
carry_on_bags, checked_bags, hide_separate_and_self_transfer,
|
|
19
|
+
exclude_basic_economy: int/bool
|
|
20
|
+
|
|
21
|
+
Errors are reported as JSON with {"error": "..."} and a non-zero exit code, so
|
|
22
|
+
the caller can distinguish "no flights" (valid empty result) from a failure.
|
|
23
|
+
|
|
24
|
+
Output shape (ResultList -> list):
|
|
25
|
+
[{ "price": int, "currency": str, "is_round_trip": bool, "airlines": [..],
|
|
26
|
+
"type": str, "carbon": {"typical_on_route": int, "emission": int},
|
|
27
|
+
"stops": int, "legs": [leg...] }]
|
|
28
|
+
|
|
29
|
+
IMPORTANT (fast-flights behavior, verified): `flight.flights` contains ONLY the
|
|
30
|
+
OUTBOUND legs. For a round-trip, `price` is the TOTAL round-trip fare, but no
|
|
31
|
+
return-leg detail is present in the parsed data. So the output describes the
|
|
32
|
+
outbound itinerary; set `is_round_trip` when return was requested so the caller
|
|
33
|
+
knows `price` is the two-way total.
|
|
34
|
+
|
|
35
|
+
Each leg: { "from": code, "from_name": str, "to": code, "to_name": str,
|
|
36
|
+
"date": "YYYY-MM-DD", "departure": "HH:MM", "arrival": "HH:MM",
|
|
37
|
+
"duration_minutes": int, "plane": str }
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import json
|
|
42
|
+
import sys
|
|
43
|
+
|
|
44
|
+
from fast_flights import (
|
|
45
|
+
FlightsNotFound,
|
|
46
|
+
FlightQuery,
|
|
47
|
+
Passengers,
|
|
48
|
+
create_query,
|
|
49
|
+
get_flights,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
SEATS = ("economy", "premium-economy", "business", "first")
|
|
53
|
+
TRIPS = ("one-way", "round-trip")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _hhmm(t) -> str:
|
|
57
|
+
return f"{t[0]:02d}:{t[1]:02d}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _leg(seg) -> dict:
|
|
61
|
+
return {
|
|
62
|
+
"from": seg.from_airport.code,
|
|
63
|
+
"from_name": seg.from_airport.name,
|
|
64
|
+
"to": seg.to_airport.code,
|
|
65
|
+
"to_name": seg.to_airport.name,
|
|
66
|
+
"date": "%04d-%02d-%02d" % seg.departure.date,
|
|
67
|
+
"departure": _hhmm(seg.departure.time),
|
|
68
|
+
"arrival": _hhmm(seg.arrival.time),
|
|
69
|
+
"duration_minutes": seg.duration,
|
|
70
|
+
"plane": seg.plane_type,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def run(q: dict) -> dict:
|
|
75
|
+
seat = q.get("seat", "economy")
|
|
76
|
+
trip = q.get("trip", "one-way")
|
|
77
|
+
if seat not in SEATS:
|
|
78
|
+
raise ValueError(f"seat must be one of {SEATS}")
|
|
79
|
+
if trip not in TRIPS:
|
|
80
|
+
raise ValueError(f"trip must be one of {TRIPS}")
|
|
81
|
+
|
|
82
|
+
outbound_date = q["outbound_date"]
|
|
83
|
+
src = q["from_airport"].strip().upper()
|
|
84
|
+
dst = q["to_airport"].strip().upper()
|
|
85
|
+
for code in (src, dst):
|
|
86
|
+
if not (code.isalpha() and len(code) == 3):
|
|
87
|
+
raise ValueError(f"airport code must be 3 letters, got {code!r}")
|
|
88
|
+
|
|
89
|
+
legs = [FlightQuery(date=outbound_date, from_airport=src, to_airport=dst)]
|
|
90
|
+
return_date = q.get("return_date")
|
|
91
|
+
if trip == "round-trip":
|
|
92
|
+
if not return_date:
|
|
93
|
+
raise ValueError("return_date is required for round-trip")
|
|
94
|
+
legs.append(FlightQuery(date=return_date, from_airport=dst, to_airport=src))
|
|
95
|
+
|
|
96
|
+
query = create_query(
|
|
97
|
+
flights=legs,
|
|
98
|
+
trip=trip,
|
|
99
|
+
seat=seat,
|
|
100
|
+
passengers=Passengers(
|
|
101
|
+
adults=int(q.get("adults", 1)),
|
|
102
|
+
children=int(q.get("children", 0)),
|
|
103
|
+
infants_in_seat=int(q.get("infants_in_seat", 0)),
|
|
104
|
+
infants_on_lap=int(q.get("infants_on_lap", 0)),
|
|
105
|
+
),
|
|
106
|
+
currency=q.get("currency", ""),
|
|
107
|
+
language=q.get("language", ""),
|
|
108
|
+
max_stops=q.get("max_stops"),
|
|
109
|
+
max_price=q.get("max_price"),
|
|
110
|
+
carry_on_bags=int(q.get("carry_on_bags", 0)),
|
|
111
|
+
checked_bags=int(q.get("checked_bags", 0)),
|
|
112
|
+
hide_separate_and_self_transfer=bool(q.get("hide_separate_and_self_transfer", False)),
|
|
113
|
+
exclude_basic_economy=bool(q.get("exclude_basic_economy", False)),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result = get_flights(query)
|
|
118
|
+
except FlightsNotFound:
|
|
119
|
+
return {"currency": q.get("currency", "") or "default", "flights": []}
|
|
120
|
+
|
|
121
|
+
currency = q.get("currency", "") or "default"
|
|
122
|
+
out = [
|
|
123
|
+
{
|
|
124
|
+
"price": f.price,
|
|
125
|
+
"currency": currency,
|
|
126
|
+
"is_round_trip": trip == "round-trip",
|
|
127
|
+
"airlines": f.airlines,
|
|
128
|
+
"type": f.type,
|
|
129
|
+
"carbon": {
|
|
130
|
+
"typical_on_route": f.carbon.typical_on_route,
|
|
131
|
+
"emission": f.carbon.emission,
|
|
132
|
+
},
|
|
133
|
+
"stops": len(f.flights) - 1,
|
|
134
|
+
"legs": [_leg(x) for x in f.flights],
|
|
135
|
+
}
|
|
136
|
+
for f in result
|
|
137
|
+
]
|
|
138
|
+
return {"currency": currency, "flights": out}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main() -> int:
|
|
142
|
+
try:
|
|
143
|
+
q = json.load(sys.stdin)
|
|
144
|
+
payload = run(q)
|
|
145
|
+
except Exception as e: # noqa: BLE001 - report any failure to the caller
|
|
146
|
+
print(json.dumps({"error": str(e)}))
|
|
147
|
+
return 1
|
|
148
|
+
print(json.dumps(payload))
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
raise SystemExit(main())
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "find-flight",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Google Flights search for agents — a pi extension (search_flights tool) + portable skill, powered by the fast-flights scraper.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"flights",
|
|
8
|
+
"google-flights",
|
|
9
|
+
"travel",
|
|
10
|
+
"flight-search",
|
|
11
|
+
"fast-flights",
|
|
12
|
+
"trip"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/fcan-dev/find-flight.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/fcan-dev/find-flight#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/fcan-dev/find-flight/issues"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"extensions/",
|
|
26
|
+
"skills/",
|
|
27
|
+
"flight_runner.py",
|
|
28
|
+
"examples/",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"verify": "python3 -c \"import json,sys; from pathlib import Path; Path('flight_runner.py')\" && python3 -m py_compile flight_runner.py && echo \"runner OK\"",
|
|
34
|
+
"prepublishOnly": "npm run verify"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"pi": {
|
|
40
|
+
"extensions": ["./extensions"],
|
|
41
|
+
"skills": ["./skills"]
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@earendil-works/pi-ai": "*",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
46
|
+
"typebox": "*"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: find-flight
|
|
3
|
+
description: Use when searching for flights, checking flight availability or prices, building flight queries, or booking research. Wraps the fast-flights Python library (Google Flights scraper) — structured queries via create_query/FlightQuery, natural-language queries, or returning ResultList flight data through get_flights.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Flight Skill
|
|
7
|
+
|
|
8
|
+
> Search Google Flights from Python using the **fast-flights** library. Build a query (structured or natural language), fetch results, and read `ResultList` flight data.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
`fast-flights` is a Python Google Flights scraper. An agent uses it by writing small Python programs: construct a search via `create_query` (or a plain sentence), call `get_flights`, and iterate the returned `ResultList`. Everything is strongly typed.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install fast-flights
|
|
16
|
+
# deps pulled automatically: primp, protobuf>=5.27, selectolax (Python >= 3.10)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## When to Use
|
|
20
|
+
|
|
21
|
+
- Find flights/times/prices between airports for a date
|
|
22
|
+
- Compare prices across dates, seats, or filter by stops/airlines/layovers
|
|
23
|
+
- Travel-planning or trip-recommendation tasks that need real availability
|
|
24
|
+
- Getting per-segment legs, durations, plane types, or carbon emissions
|
|
25
|
+
|
|
26
|
+
**When NOT:** for booking/reservation actual purchase (this is read-only search), or non-flight transport.
|
|
27
|
+
|
|
28
|
+
## Before you query (REQUIRED gate)
|
|
29
|
+
|
|
30
|
+
**Never guess the date, the trip type, or the airport codes.** All three are common model failure points — resolve them *before* building any query:
|
|
31
|
+
|
|
32
|
+
1. **Get the real current date from the system** — run `date` in a shell (or read the runtime clock), not from memory:
|
|
33
|
+
```bash
|
|
34
|
+
date "+%Y-%m-%d"
|
|
35
|
+
```
|
|
36
|
+
Then resolve relative/ambiguous dates ("next Friday", "11-13 September") against this real year. Format every date as `YYYY-MM-DD`.
|
|
37
|
+
2. **Confirm one-way vs round-trip.** If the user didn't explicitly say, ASK: one-way (1 leg, needs only `date`+`from`+`to`) vs round-trip (2+ legs, needs a `return_date` too). Do not assume. If a stay duration or "until" date is given, that implies round-trip — but still confirm the dates.
|
|
38
|
+
3. **Verify the airport codes — never guess what a 3-letter IATA code means.** Models routinely map codes to the wrong city (e.g. misreading `ECN` as Edinburgh when it is really Ercan, or inventing a code). Before searching, confirm what `from_airport`/`to_airport` actually are: look the code up in the upstream airport list (`enums/airports.csv` in `AWeirdDev/flights`, ~9.7k rows, by code + name) or against a known route. If you can't verify a code, ask the user instead of assuming.
|
|
39
|
+
|
|
40
|
+
## Core Recipe — structured query
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from fast_flights import (
|
|
44
|
+
FlightQuery, Passengers, ResultList,
|
|
45
|
+
create_query, get_flights, FlightsNotFound,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
query = create_query(
|
|
49
|
+
flights=[
|
|
50
|
+
FlightQuery( # one FlightQuery per leg
|
|
51
|
+
date="2025-01-01", # "YYYY-MM-DD" or a datetime
|
|
52
|
+
from_airport="TPE", # 3-letter IATA code
|
|
53
|
+
to_airport="MYJ",
|
|
54
|
+
max_stops=1,
|
|
55
|
+
airlines=["JL", "ONEWORLD"], # IATA codes, or SKYTEAM/STAR_ALLIANCE/ONEWORLD
|
|
56
|
+
earliest_departure_hour=7, # local time, 0-23
|
|
57
|
+
latest_departure_hour=18,
|
|
58
|
+
earliest_arrival_hour=10,
|
|
59
|
+
latest_arrival_hour=23,
|
|
60
|
+
max_duration_minutes=720, # minutes
|
|
61
|
+
connecting_airports=["HND", "NRT"],
|
|
62
|
+
min_layover_minutes=60,
|
|
63
|
+
max_layover_minutes=240,
|
|
64
|
+
less_emissions_only=True,
|
|
65
|
+
),
|
|
66
|
+
],
|
|
67
|
+
seat="economy", # economy | premium-economy | business | first
|
|
68
|
+
trip="one-way", # one-way | round-trip (multi-city unimplemented)
|
|
69
|
+
passengers=Passengers(adults=1), # adults/children/infants_in_seat/infants_on_lap
|
|
70
|
+
language="en-US", # "" lets Google decide
|
|
71
|
+
currency="USD", # "" lets Google decide
|
|
72
|
+
max_price=1500, # whole-search filters ↓
|
|
73
|
+
carry_on_bags=0,
|
|
74
|
+
checked_bags=0,
|
|
75
|
+
hide_separate_and_self_transfer=False,
|
|
76
|
+
exclude_basic_economy=False,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
result: ResultList = get_flights(query)
|
|
81
|
+
for flight in result:
|
|
82
|
+
print(flight.price, flight.airlines, flight.type)
|
|
83
|
+
except FlightsNotFound:
|
|
84
|
+
print("no flights found")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Round-trip example
|
|
88
|
+
|
|
89
|
+
A round-trip is two `FlightQuery` legs in one `create_query` call — outbound then return (reverse airports, return date):
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
query = create_query(
|
|
93
|
+
flights=[
|
|
94
|
+
FlightQuery(date="2025-01-01", from_airport="IST", to_airport="ECN"),
|
|
95
|
+
FlightQuery(date="2025-01-08", from_airport="ECN", to_airport="IST"),
|
|
96
|
+
],
|
|
97
|
+
trip="round-trip",
|
|
98
|
+
seat="economy",
|
|
99
|
+
passengers=Passengers(adults=1),
|
|
100
|
+
currency="EUR",
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Natural-language path (agent-friendly)
|
|
105
|
+
|
|
106
|
+
`get_flights` also accepts a plain sentence instead of a `Query`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from fast_flights import get_flights
|
|
110
|
+
result = get_flights(
|
|
111
|
+
"Flights from TPE to MYJ on 2025-01-01 one way economy class"
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Preferred when the request arrives as English text; use the structured recipe when the user gives discrete filters.
|
|
116
|
+
|
|
117
|
+
## Reading results — the data model
|
|
118
|
+
|
|
119
|
+
`ResultList` is a list of `Flights`, with metadata attached.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
result.metadata # JsMetadata: .airlines[], .alliances[] (code+name)
|
|
123
|
+
for flight in result:
|
|
124
|
+
# flight: Flights
|
|
125
|
+
flight.price # int, in selected currency
|
|
126
|
+
flight.airlines # list[str] of IATA codes
|
|
127
|
+
flight.type # str | "multi"
|
|
128
|
+
flight.carbon # CarbonEmission: .typical_on_route, .emission (grams)
|
|
129
|
+
for leg in flight.flights: # SingleFlight
|
|
130
|
+
leg.from_airport # Airport: .code, .name
|
|
131
|
+
leg.to_airport # Airport: .code, .name
|
|
132
|
+
leg.departure # SimpleDatetime: .date=(Y,M,D), .time=(H,M)
|
|
133
|
+
leg.arrival # SimpleDatetime: .date=(Y,M,D), .time=(H,M)
|
|
134
|
+
leg.duration # int, minutes
|
|
135
|
+
leg.plane_type # str
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Currency is NOT attached to prices.** `flight.price` is a bare `int` with no currency symbol or code. The currency that the price is quoted in is whatever you passed to `create_query(currency=...)` (or Google's default if you passed `""` — often the local currency, e.g. Turkish Lira for a Turkey-originating route). When presenting prices, surface the currency explicitly: track the `currency` you passed (or the user's expected currency) and label every price with it. Never hardcode a currency symbol in your output format — it may not match the price's actual currency.
|
|
139
|
+
|
|
140
|
+
**Round-trip: `price` is the total, but only OUTBOUND legs are returned (verified).** For a `round-trip` query, `flight.flights` contains **only the outbound legs** — the parsed data does not include return-leg detail. `flight.price` is the **two-way total fare**. So: count stops and display legs from `flight.flights` as the outbound itinerary, and tell the user that `price` covers both directions but only outbound flight times are available. Do NOT invent or assume return-leg segments that aren't in the data.
|
|
141
|
+
|
|
142
|
+
`FlightsNotFound` is raised when nothing matches (or Google returns an error status).
|
|
143
|
+
|
|
144
|
+
### Sorting / narrowing results
|
|
145
|
+
|
|
146
|
+
`ResultList` is a plain list.sort()-able list — sort by price or total duration before presenting to the user:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
def total_duration(flight):
|
|
150
|
+
return sum(seg.duration for seg in flight.flights)
|
|
151
|
+
|
|
152
|
+
result.sort(key=lambda f: f.price) # cheapest first
|
|
153
|
+
result.sort(key=total_duration) # shortest trip first
|
|
154
|
+
best = result[0] # or result[:3] for top-N
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Options reference
|
|
158
|
+
|
|
159
|
+
| Option | Where | Values / notes |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `seat` | create_query | `economy`, `premium-economy`, `business`, `first` |
|
|
162
|
+
| `trip` | create_query | `one-way`, `round-trip`. **round-trip needs 2+ `FlightQuery` legs**; `multi-city` unimplemented |
|
|
163
|
+
| `passengers` | create_query | `Passengers(adults, children, infants_in_seat, infants_on_lap)` |
|
|
164
|
+
| `language` / `currency` | create_query | any supported IETF tag / ISO code; `""` = Google default |
|
|
165
|
+
| `max_price`, `carry_on_bags`, `checked_bags`, `hide_separate_and_self_transfer`, `exclude_basic_economy` | create_query | search-wide |
|
|
166
|
+
| Per-leg filters | FlightQuery | `max_stops`, `airlines`, departure/arrival hour windows, `max_duration_minutes`, `connecting_airports`, `min/max_layover_minutes`, `less_emissions_only` |
|
|
167
|
+
|
|
168
|
+
## Gotchas
|
|
169
|
+
|
|
170
|
+
- **Hours use local airport time** on a 0–23 clock; durations/layovers are in minutes.
|
|
171
|
+
- **`airlines` on non-first legs is ignored** — Google applies only the first leg's airline filter to the whole search. (The filter also accepts an alliance name.)
|
|
172
|
+
- **`max_stops` can be passed once on `create_query`** as a global, and/or per-leg via `FlightQuery` (per-leg wins).
|
|
173
|
+
- **Round-trip = two+ `FlightQuery` objects**; one way = one.
|
|
174
|
+
- **Passengers rules:** total ≤ 9, and `infants_on_lap ≤ adults` (one lap infant needs one adult). Violations raise `AssertionError`.
|
|
175
|
+
- **Read-only:** this scrapes availability; it cannot book. No guarantee against rate limits / IP blocks — proxy via an integration if scraping at scale.
|
|
176
|
+
- **Live network dependency:** `get_flights` hits Google's servers at call time. For repeatable/offline testing, use `fetch_flights_html` or cached HTML, or an integration (BrightData / SearchApi).
|
|
177
|
+
|
|
178
|
+
## Integrations (optional)
|
|
179
|
+
|
|
180
|
+
- **BrightData** — proxy the default fetcher to protect your IP (pass `integration=BrightData(api_key=...)`). Only needed if scraping at scale.
|
|
181
|
+
- **SearchApi** — richer data source. Returns `SearchApiResult` (booking options, price insights, amenities) instead of `ResultList`:
|
|
182
|
+
```python
|
|
183
|
+
from fast_flights.integrations import SearchApi
|
|
184
|
+
result = get_flights(..., integration=SearchApi())
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Common Mistakes
|
|
188
|
+
|
|
189
|
+
| Mistake | Fix |
|
|
190
|
+
|---|---|
|
|
191
|
+
| Using `get_flights` without try/except | Wrap in `try/except FlightsNotFound` |
|
|
192
|
+
| Round-trip with one leg | Add a second `FlightQuery` (e.g. return leg) |
|
|
193
|
+
| Expecting `multi-city` to work | Unimplemented — treat as not supported today |
|
|
194
|
+
| Passing `airlines` to multiple legs expecting per-leg filtering | Only first leg's `airlines` is applied |
|
|
195
|
+
| Assuming 24h timestamps | Times are `(hour, minute)` on a 0–23 clock |
|
|
196
|
+
| Guessing what a 3-letter airport code means | Verify it against the upstream `enums/airports.csv` (or a known route) before searching |
|
|
197
|
+
| Hardcoding a currency symbol in output | `price` has no currency — track/pass `currency` and label prices with it |
|
|
198
|
+
| Assuming return legs are in the round-trip data | `flights` holds only outbound legs; `price` is the two-way total — label it as such |
|
|
199
|
+
|
|
200
|
+
## Real-world notes
|
|
201
|
+
|
|
202
|
+
- Version: `fast-flights` 3.x. See upstream repo `AWeirdDev/flights` and docs `aweirddev.github.io/flights`.
|
|
203
|
+
- Run a ready-made CLI: `python examples/search_flights.py --from TPE --to MYJ --date 2025-01-01 --seat economy`.
|
|
204
|
+
- **Round-trip data (verified):** the parsed result contains only outbound legs; `price` is the two-way total. Present it as such.
|
|
205
|
+
- **pi extension:** if the `search_flights` custom tool is active (via `extensions/flight-search.ts`), prefer calling it over writing ad-hoc Python — it returns structured, correctly-labelled results and leaves no scratch files. `flight_runner.py` is the equivalent standalone executor.
|