Skip to main content
Glama
README.md
# 🛫 Fli - Flight Search MCP Server and Library

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/punitarani/fli)

A powerful Python library that provides programmatic access to Google Flights data with an elegant CLI interface. Search
flights, find the best deals, and filter results with ease.

> 🚀 **What makes `fli` special?**
> Unlike other flight search libraries that rely on web scraping, Fli directly interacts with Google Flights' API
> through reverse engineering.
> This means:
>
> * **Fast**: Direct API access means faster, more reliable results
> * **Zero Scraping**: No HTML parsing, no browser automation, just pure API interaction
> * **Reliable**: Less prone to breaking from UI changes
> * **Modular**: Extensible architecture for easy customization and integration

## MCP Server

```bash
pipx install flights

# Run the MCP server on STDIO
fli-mcp

# Run the MCP server over HTTP (streamable)
fli-mcp-http  # serves at http://127.0.0.1:8000/mcp/
```

![MCP Demo](https://raw.githubusercontent.com/punitarani/fli/main/docs/assets/mcp-demo.gif)

### Connecting to Claude Desktop

```json
{
  "mcpServers": {
    "fli": {
      "command": "/Users/<user>/.local/bin/fli-mcp"
    }
  }
}
```

> **Note**: Replace `<user>` with your actual username.
> You can also find the path to the MCP server by running `which fli-mcp` in your terminal.

### MCP Tools Available

The MCP server provides two main tools:

| Tool                 | Description                                                 |
|----------------------|-------------------------------------------------------------|
| **`search_flights`** | Search for flights on a specific date with detailed filters |
| **`search_dates`**   | Find the cheapest travel dates across a flexible date range |

#### `search_flights` Parameters

| Parameter           | Type   | Description                                                 |
|---------------------|--------|-------------------------------------------------------------|
| `origin`            | string | Departure airport IATA code(s) — comma-separated for multi  |
| `destination`       | string | Arrival airport IATA code(s) — comma-separated for multi    |
| `departure_date`    | string | Travel date in YYYY-MM-DD format                            |
| `return_date`       | string | Return date for round trips (optional)                      |
| `cabin_class`       | string | ECONOMY, PREMIUM_ECONOMY, BUSINESS, or FIRST                |
| `max_stops`         | string | ANY, NON_STOP, ONE_STOP, or TWO_PLUS_STOPS                  |
| `departure_window`  | string | Time window in 'HH-HH' format (e.g., '6-20')                |
| `airlines`          | list   | Filter by airline codes (e.g., ['BA', 'AA'])                |
| `exclude_airlines`  | list   | Airline IATA codes to **exclude** (e.g., ['DL', 'B6'])      |
| `alliance`          | list   | Restrict to alliances: ONEWORLD, SKYTEAM, STAR_ALLIANCE     |
| `exclude_alliance`  | list   | Alliance names to **exclude** from results                  |
| `min_layover`       | int    | Minimum layover duration in minutes (multi-stop only)       |
| `max_layover`       | int    | Maximum layover duration in minutes (multi-stop only)       |
| `top_n`             | int    | Round-trip only: outbound options expanded into return flights (default 5, 1-10) |
| `currency`          | string | ISO 4217 code (e.g. 'EUR', 'JPY') — flows to `curr=` param  |
| `language`          | string | BCP-47 language code (e.g. 'en-GB') — flows to `hl=` param  |
| `country`           | string | ISO 3166-1 alpha-2 country code (e.g. 'GB') for `gl=` param |
| `sort_by`           | string | CHEAPEST, DURATION, DEPARTURE_TIME, or ARRIVAL_TIME         |
| `passengers`        | int    | Number of adult passengers                                  |
| `children`          | int    | Number of children (ages 2-11)                               |
| `infants_in_seat`   | int    | Number of infants (under 2) occupying their own seat        |
| `infants_on_lap`    | int    | Number of lap infants (under 2, no seat)                    |

> Total travelers (`passengers + children + infants_in_seat + infants_on_lap`) must be
> between 1 and 9, and `infants_on_lap` cannot exceed `passengers`.
>
> A round trip costs `1 + top_n` page fetches (one outbound search, plus one per
> outbound candidate expanded into return flights), so `top_n` is capped at 10. Round-trip
> results all from one airline? Raise `top_n`, or sort differently — the default sort only
> ever expands the cheapest `top_n` outbounds, which are often the same carrier.

#### `search_dates` Parameters

| Parameter           | Type   | Description                                                 |
|---------------------|--------|-------------------------------------------------------------|
| `origin`            | string | Departure airport IATA code(s) — comma-separated for multi  |
| `destination`       | string | Arrival airport IATA code(s) — comma-separated for multi    |
| `start_date`        | string | Start of date range in YYYY-MM-DD format                    |
| `end_date`          | string | End of date range in YYYY-MM-DD format                      |
| `trip_duration`     | int    | Trip duration in days (for round-trips)                     |
| `is_round_trip`     | bool   | Whether to search for round-trip flights                    |
| `cabin_class`       | string | ECONOMY, PREMIUM_ECONOMY, BUSINESS, or FIRST                |
| `max_stops`         | string | ANY, NON_STOP, ONE_STOP, or TWO_PLUS_STOPS                  |
| `departure_window`  | string | Time window in 'HH-HH' format (e.g., '6-20')                |
| `airlines`          | list   | Filter by airline codes (e.g., ['BA', 'AA'])                |
| `exclude_airlines`  | list   | Airline IATA codes to **exclude**                           |
| `alliance`          | list   | Restrict to alliances: ONEWORLD, SKYTEAM, STAR_ALLIANCE     |
| `exclude_alliance`  | list   | Alliance names to **exclude**                               |
| `min_layover`       | int    | Minimum layover duration in minutes                         |
| `max_layover`       | int    | Maximum layover duration in minutes                         |
| `currency`          | string | ISO 4217 currency code (e.g. 'EUR', 'JPY')                  |
| `language`          | string | BCP-47 language code (e.g. 'en-GB')                         |
| `country`           | string | ISO 3166-1 alpha-2 country code (e.g. 'GB')                 |
| `sort_by_price`     | bool   | Sort results by price (lowest first)                        |
| `passengers`        | int    | Number of adult passengers                                  |
| `children`          | int    | Number of children (ages 2-11)                               |
| `infants_in_seat`   | int    | Number of infants (under 2) occupying their own seat        |
| `infants_on_lap`    | int    | Number of lap infants (under 2, no seat)                    |

> Same passenger limits as `search_flights`: total 1-9, `infants_on_lap` ≤ `passengers`.

## Quick Start

```bash
pip install flights
```

```bash
# Install using pipx (recommended for CLI)
pipx install flights

# Get started with CLI
fli --help
```

![CLI Demo](https://raw.githubusercontent.com/punitarani/fli/main/docs/assets/cli-demo.png)

## Features

* 🔍 **Powerful Search**
    * One-way flight searches
    * Multi-city flight searches
    * Flexible departure times
    * Multi-airline support
    * Cabin class selection
    * Stop preferences
    * Custom result sorting

* 💺 **Cabin Classes**
    * Economy
    * Premium Economy
    * Business
    * First

* 🎯 **Smart Sorting**
    * Price
    * Duration
    * Departure Time
    * Arrival Time

* 🛡️ **Built-in Protection**
    * Rate limiting
    * Automatic retries
    * Comprehensive error handling
    * Input validation

## Search transport

Searches are served by Google's public search page rather than the
`FlightsFrontendService` RPC. Since 2026-08 `GetShoppingResults` and
`GetCalendarGraph` require an `x-goog-batchexecute-bgr` header that only the
page's own JavaScript can produce, so a plain HTTP client gets HTTP 200 with no
payload. Fli issues `GET https://www.google.com/travel/flights?tfs=<protobuf>`
instead and reads the results out of the page's inline `AF_initDataCallback`
blob keyed `ds:1`.

What that means in practice:

* **Three filters are not supported.** `emissions`, `bags` and
  `exclude_basic_economy` have no `tfs` field and cannot be reconstructed from
  the decoded rows, so they are dropped with a warning. Stops, cabin,
  passengers, alliances and layover bounds ride in the request; airline
  include/exclude, price cap, max duration and departure windows are applied to
  the results after fetching.
* **Multi-city raises `SearchUnsupportedError`.** Google loads those results
  client-side through the gated RPC, so the page carries no rows to read.
  Search each leg separately.
* **`get_booking_options` is unavailable.** It calls `GetBookingResults`, which
  is gated the same way, and currently raises `SearchRejectedError`. The
  per-flight `tfs` booking deep links are built offline and still work.
* **Fewer rows per search.** Expect roughly 20-45 itineraries, fewer than the
  old RPC returned — and a client-side filter cannot back-fill the list the way
  Google's server-side one did.
* **Children and infants thin the results — sometimes to nothing.** Google
  prices those parties client-side, so the page inlines fewer itineraries for
  them, and in premium cabins often none (measured 2026-09: JFK→LHR economy
  23 rows for one adult, 16 with an infant; SFO→NRT business 9 rows for one
  or two adults, 0 with a child). Extra adults cost nothing. An empty result
  for such a search does not mean the route has no flights — and neither
  does it mean this is why: `search()` only warns when the fetched page
  itself came back with zero rows, not when the caller's own airline/price/
  duration/window filter removed rows Google did return. An adults-only
  search shows the schedule.
* **Date searches cost one page fetch per date.** The page has no calendar
  grid, so a range is priced date by date; one `SearchDates.search` covers at
  most 93 dates and a wider range raises `ValueError`. Budget for it: 93 dates
  across 10 workers is several hundred MB of pages and parsed JSON at peak.
  A sweep that never manages to load a single page — the shape a blocked or
  consent-gated client produces — gives up after a handful of dates rather than
  paying the retry budget on all of them. Only pages served without results
  count towards that: a timeout or a dropped connection says nothing about the
  dates not yet tried, so those never abandon a sweep. Measured with the real backoff: **42
  page fetches** (bounded at 45, so up to ~135 HTTP requests once the client's
  own retries multiply in) and about 4 seconds, the same whether the range is 30
  days or 93. Unbroken, a 93-date range would have cost 279 fetches and up to
  837 requests. The bound is `(5 + worker count) x 3`, so raising
  `configure_concurrency` raises it proportionally. That breaker disarms for
  good the moment any page loads, even an empty one, so it cannot catch a
  sweep that is mostly timeouts around one lucky date — `SearchDates.search`
  raises that case too, whenever nothing priced and at least half the
  attempted dates never loaded. A minority of failures alongside real
  results, or alongside a confirmed-empty range (`None`), still returns
  normally but logs one warning naming the counts.
* **A page occasionally arrives without results.** Roughly one request in sixty
  returns HTTP 200 with no `ds:1` blob; the client retries that case up to twice
  (0.5s then 1.5s) before raising `SearchParseError`. A healthy search never
  pays for it.
* **`FLI_SOCS_COOKIE`.** EU/EEA IPs are redirected to Google's consent
  interstitial, which serves no `ds:1` blob. The client sends a pre-accepted
  `SOCS` consent cookie by default; set `FLI_SOCS_COOKIE` to change the value,
  or to an empty string to send none.
* **`FLI_CA_BUNDLE` / `CURL_CA_BUNDLE` / `REQUESTS_CA_BUNDLE`.** Behind a
  TLS-intercepting corporate proxy, set one of these (checked in that order)
  to a PEM CA bundle path. A bad or missing path, or a certificate the bundle
  doesn't cover, raises `SearchCertificateError` naming the variable — the
  client does not retry it, since a fixed bundle either verifies or it doesn't.
  Read once per worker thread on its first request; changing the value
  doesn't affect a thread's already-created session.

## CLI Usage

### Search for Flights

```bash
# Basic flight search
fli flights JFK LHR 2026-10-25

# Advanced search with filters
fli flights JFK LHR 2026-10-25 \
    --time 6-20 \             # Departure time window (6 AM - 8 PM)
    --airlines BA,KL \        # Airlines (British Airways, KLM)
    --class BUSINESS \        # Cabin class
    --stops NON_STOP \        # Non-stop flights only
    --sort DURATION           # Sort by duration

# Alliance + exclude + locale (May-2026 filter additions)
fli flights JFK LHR 2026-10-25 \
    --alliance ONEWORLD \
    --exclude-airlines AA \
    --min-layover 90 \
    --max-layover 360 \
    --currency EUR --language en-GB --country GB

# Round trip: raise --top-n to see more airlines (default 5, max 10, costs 1 + top_n
# page fetches per search)
fli flights JFK LHR 2026-10-25 --return 2026-11-01 --top-n 8

# Family mix: 2 adults, 1 child, 1 lap infant (total must be 1-9)
fli flights JFK LHR 2026-10-25 --passengers 2 --children 1 --infants-on-lap 1
```

> ⚠️ **Experimental**
> `--format json` is experimental. The JSON schema may change while the machine-readable CLI contract settles.
>
> ```bash
> # Return machine-readable flight results
> fli flights JFK LHR 2026-10-25 --format json
> ```

### Find Cheapest Dates

```bash
# Basic date search
fli dates JFK LHR

# Advanced search with date range
fli dates JFK LHR \
    --from 2026-01-01 \
    --to 2026-02-01 \
    --monday --friday      # Only Mondays and Fridays
```

> ⚠️ **Experimental**
> `--format json` is experimental for date searches as well.
>
> ```bash
> # Return machine-readable date search results
> fli dates JFK LHR --from 2026-01-01 --to 2026-02-01 --format json
> ```

### Multi-city Search

```bash
# Two-leg multi-city trip
fli multi --leg SEA,HKG,2026-12-26 --leg PEK,SEA,2027-01-02

# Three-leg multi-city trip with filters
fli multi \
    -l SEA,NRT,2026-12-26 \
    -l NRT,HKG,2026-12-30 \
    -l HKG,SEA,2027-01-05 \
    --class BUSINESS \
    --stops 0
```

### CLI Options

#### Flights Command (`fli flights`)

| Option                  | Description                                | Example                          |
|-------------------------|--------------------------------------------|----------------------------------|
| `--return, -r`          | Return date                                | `2026-10-30`                     |
| `--time, -t`            | Departure time window                      | `6-20`                           |
| `--airlines, -a`        | Airline IATA codes                         | `BA,KL`                          |
| `--exclude-airlines, -A` | Airline IATA codes to **exclude**         | `DL,B6`                          |
| `--alliance`            | Restrict to alliance(s)                    | `ONEWORLD`, `SKYTEAM`            |
| `--exclude-alliance`    | Alliance(s) to **exclude**                 | `STAR_ALLIANCE`                  |
| `--min-layover`         | Minimum layover (minutes)                  | `90`                             |
| `--max-layover`         | Maximum layover (minutes)                  | `360`                            |
| `--top-n`               | Round-trip only: outbound options expanded into return flights (default 5, 1-10) | `8` |
| `--currency`            | ISO 4217 currency code                     | `EUR`, `JPY`                     |
| `--language`            | BCP-47 language code (Google `hl=`)        | `en-GB`                          |
| `--country`             | ISO 3166-1 alpha-2 country (`gl=`)         | `GB`                             |
| `--class, -c`           | Cabin class                                | `ECONOMY`, `BUSINESS`            |
| `--stops, -s`           | Maximum stops                              | `NON_STOP`, `ONE_STOP`           |
| `--sort, -o`            | Sort results by                            | `CHEAPEST`, `DURATION`           |
| `--passengers, -p`      | Number of adult passengers                 | `2`                               |
| `--children`            | Number of children (ages 2-11)             | `1`                               |
| `--infants-in-seat`     | Number of infants occupying their own seat | `1`                               |
| `--infants-on-lap`      | Number of lap infants (must be ≤ adults)   | `1`                               |
| `--format`              | Output format                              | `text`, `json`                   |

> Total passengers (adults + children + infants) must be between 1 and 9.
>
> `--top-n` only applies to round trips (it is rejected if set on a one-way search) and
> costs `1 + top_n` page fetches. Round-trip results all from one airline? Raise `--top-n`
> (up to 10), or `--sort` differently — the default sort only expands the cheapest
> `top_n` outbounds, which are often the same carrier.

#### Dates Command (`fli dates`)

| Option                  | Description                                | Example                  |
|-------------------------|--------------------------------------------|--------------------------|
| `--from`                | Start date                                 | `2026-01-01`             |
| `--to`                  | End date                                   | `2026-02-01`             |
| `--duration, -d`        | Trip duration in days                      | `3`                      |
| `--round, -R`           | Round-trip search                          | (flag)                   |
| `--airlines, -a`        | Airline IATA codes                         | `BA,KL`                  |
| `--exclude-airlines, -A`| Airline IATA codes to **exclude**          | `DL,B6`                  |
| `--alliance`            | Restrict to alliance(s)                    | `ONEWORLD`               |
| `--exclude-alliance`    | Alliance(s) to **exclude**                 | `STAR_ALLIANCE`          |
| `--min-layover`         | Minimum layover (minutes)                  | `90`                     |
| `--max-layover`         | Maximum layover (minutes)                  | `360`                    |
| `--currency`            | ISO 4217 currency code                     | `EUR`, `JPY`             |
| `--language`            | BCP-47 language code                       | `en-GB`                  |
| `--country`             | ISO 3166-1 alpha-2 country                 | `GB`                     |
| `--class, -c`           | Cabin class                                | `ECONOMY`, `BUSINESS`    |
| `--stops, -s`           | Maximum stops                              | `NON_STOP`, `ONE_STOP`   |
| `--time`                | Departure time window                      | `6-20`                   |
| `--sort`                | Sort by price                              | (flag)                   |
| `--[day]`               | Day filters                                | `--monday`, `--friday`   |
| `--passengers, -p`      | Number of adult passengers                 | `2`                       |
| `--children`            | Number of children (ages 2-11)             | `1`                       |
| `--infants-in-seat`     | Number of infants occupying their own seat | `1`                       |
| `--infants-on-lap`      | Number of lap infants (must be ≤ adults)   | `1`                       |
| `--format`              | Output format                              | `text`, `json`           |

#### Multi Command (`fli multi`)

| Option           | Description                          | Example                        |
|------------------|--------------------------------------|--------------------------------|
| `--leg, -l`      | Flight leg (ORIGIN,DEST,DATE format) | `SEA,HKG,2026-12-26`          |
| `--time, -t`     | Departure time window                | `6-20`                         |
| `--airlines, -a` | Airline IATA codes                   | `DL CX`                       |
| `--class, -c`    | Cabin class                          | `ECONOMY`, `BUSINESS`          |
| `--stops, -s`    | Maximum stops                        | `NON_STOP`, `ONE_STOP`         |
| `--sort, -o`     | Sort results by                      | `CHEAPEST`, `DURATION`         |
| `--passengers, -p` | Number of adult passengers         | `2`                             |
| `--children`     | Number of children (ages 2-11)       | `1`                             |
| `--infants-in-seat` | Number of infants occupying their own seat | `1`                     |
| `--infants-on-lap` | Number of lap infants (must be ≤ adults) | `1`                       |

## MCP Server Integration

Fli includes a Model Context Protocol (MCP) server that allows AI assistants like Claude to search for flights directly.
This enables natural language flight search through conversation.

### Running the MCP Server

```bash
# Run the MCP server on STDIO
fli-mcp

# Or with uv (for development)
uv run fli-mcp

# Or with make (for development)
make mcp

# Run the MCP server over HTTP (streamable)
fli-mcp-http  # serves at http://127.0.0.1:8000/mcp/
```

### Claude Desktop Configuration

To use the flight search capabilities in Claude Desktop, add this configuration to your `claude_desktop_config.json`:

**Location**: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)

```json
{
  "mcpServers": {
    "flight-search": {
      "command": "fli-mcp",
      "args": []
    }
  }
}
```

After adding this configuration:

1. Restart Claude Desktop
2. You can now ask Claude to search for flights naturally:
    * "Find flights from JFK to LAX on December 25th"
    * "What are the cheapest dates to fly from NYC to London in January?"
    * "Search for business class flights from SFO to NRT with no stops"

## Python API Usage

### Basic Search Example

```python
from datetime import datetime, timedelta
from fli.models import (
    Airport,
    PassengerInfo,
    SeatType,
    MaxStops,
    SortBy,
    FlightSearchFilters,
    FlightSegment
)
from fli.search import SearchFlights

# Create search filters
filters = FlightSearchFilters(
    passenger_info=PassengerInfo(adults=1),
    flight_segments=[
        FlightSegment(
            departure_airport=[[Airport.JFK, 0]],
            arrival_airport=[[Airport.LAX, 0]],
            travel_date=(datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d"),
        )
    ],
    seat_type=SeatType.ECONOMY,
    stops=MaxStops.NON_STOP,
    sort_by=SortBy.CHEAPEST,
)

# Search flights
search = SearchFlights()
flights = search.search(filters)

# Process results
for flight in flights:
    print(f"💰 Price: ${flight.price}")
    print(f"⏱️ Duration: {flight.duration} minutes")
    print(f"✈️ Stops: {flight.stops}")

    for leg in flight.legs:
        print(f"\n🛫 Flight: {leg.airline.value} {leg.flight_number}")
        print(f"📍 From: {leg.departure_airport.value} at {leg.departure_datetime}")
        print(f"📍 To: {leg.arrival_airport.value} at {leg.arrival_datetime}")
```

### Running Examples

Runnable examples live in [`examples/python/`](examples/python):

```bash
# Run examples with uv (recommended)
uv run python examples/python/basic_one_way_search.py
uv run python examples/python/round_trip_search.py
uv run python examples/python/date_range_search.py

# Or install dependencies first, then run directly
pip install pydantic curl_cffi httpx
python examples/python/basic_one_way_search.py
```

**Available Examples:**

* `basic_one_way_search.py` - Simple one-way flight search
* `round_trip_search.py` - Round-trip flight booking
* `date_range_search.py` - Find cheapest dates
* `multi_city_search.py` - Multi-city itinerary across several legs
* `advanced_filters_search.py` - Alliances, airline exclusions, layovers, locale
* `complex_flight_search.py` - Advanced filtering and multi-passenger
* `time_restrictions_search.py` - Time-based filtering
* `date_search_with_preferences.py` - Weekend filtering
* `price_tracking.py` - Price monitoring over time
* `error_handling_with_retries.py` - Robust error handling
* `result_processing.py` - Data analysis with pandas
* `complex_round_trip_validation.py` - Advanced round-trip with validation
* `advanced_date_search_validation.py` - Complex date search with filtering

## Examples

Examples are organized by language, with parallel scripts so you can compare the
two APIs:

* **Python** — [`examples/python/`](examples/python)
* **TypeScript** — [`examples/typescript/`](examples/typescript)

```bash
# Python
uv run python examples/python/complex_flight_search.py

# TypeScript (from examples/typescript, after `bun install`)
bun run multi_city_search.ts
```

**Example Categories:**

* **Basic Usage**: One-way, round-trip, date searches
* **Advanced Filtering**: Time restrictions, airlines, alliances, seat classes, locale
* **Data Analysis**: Price tracking, result processing with pandas
* **Error Handling**: Retry logic, robust error management
* **Complex Scenarios**: Multi-city, multi-passenger, validation, business rules

Each example is self-contained — change the airports, dates, and filters at the top of the script to fit your search.

## TypeScript / JavaScript

Fli is also available as a 1:1 TypeScript port, published to npm as
[`fli-js`](https://www.npmjs.com/package/fli-js). Same models, same filter encoding,
same direct-API approach.

```bash
bun add fli-js   # or: npm install fli-js
```

```ts
import { Airport, FlightSearchFilters, FlightSegment, SearchFlights, SeatType } from "fli-js";

const inDays = (n: number) => new Date(Date.now() + n * 86_400_000).toISOString().slice(0, 10);

const filters = new FlightSearchFilters({
  passenger_info: { adults: 1, children: 0, infants_in_seat: 0, infants_on_lap: 0 },
  flight_segments: [
    new FlightSegment({
      departure_airport: [[[Airport.JFK, 0]]],
      arrival_airport: [[[Airport.LAX, 0]]],
      travel_date: inDays(30),
    }),
  ],
  seat_type: SeatType.ECONOMY,
});

const results = await new SearchFlights().search(filters, { currency: "USD" });
```

`fli-js` uses the same search-page transport as the Python package (see
[Search transport](#search-transport) above), with the same consequences:
`emissions` / `bags` / `exclude_basic_economy` are dropped with a warning,
multi-city throws `SearchUnsupportedError`, `getBookingOptions` throws
`SearchRejectedError`, date searches cost one page fetch per date and are
capped at 93, and `FLI_SOCS_COOKIE` controls the consent cookie.

The TypeScript source lives in [`fli-js/`](fli-js); see the
[TypeScript Quick Start](https://punitarani.github.io/fli/typescript/quickstart/)
for the full guide.

## Development

```bash
# Clone the repository
git clone https://github.com/punitarani/fli.git
cd fli

# Install dependencies with uv
uv sync --all-extras

# Run tests (offline only — no network access required)
uv run pytest

# Run linting
uv run ruff check .
uv run ruff format .

# Build documentation
uv run mkdocs serve

# Or use the Makefile for common tasks
make install-all  # Install all dependencies
make test         # Run tests (offline only)
make test-live    # Run the small, stable live tests (real network)
make lint         # Check code style
make format       # Format code
```

Tests that call the real Google Flights API are marked `live` and skipped by
default (`--fuzz`, `--live` and `--all` are independent, skip-only gates —
`--all` does *not* imply `--live`, and `--live` alone does *not* imply
`--fuzz`). Run the small, stable live set with `make test-live` (or
`pytest -m live --live`); the noisier 100-case fuzz-gated live test is a
separate opt-in, `make test-live-fuzz` (`pytest --all -m live --live
tests/search/test_search_flights_fuzz.py` — it needs `--all`/`--fuzz`
*together with* `--live`, since it carries both markers).

### Docker Development

```bash
# Build the devcontainer
docker build -t fli-dev -f .devcontainer/Dockerfile .

# Run CI inside the container
docker run --rm fli-dev make lint test-all

# Or run lint and tests separately
docker run --rm fli-dev make lint
docker run --rm fli-dev make test-all
```

### Running CI Locally with act

To run GitHub Actions locally, install [act](https://github.com/nektos/act):

```bash
brew install act

# Run CI locally (lint + tests on Python 3.10-3.13)
make ci

# Or run CI inside Docker (no local act installation needed)
make ci-docker
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the MIT License — see the LICENSE file for details.