Skip to content

Seats.aero as second AwardProvider (work-2eoa)#16

Merged
ak2k merged 2 commits into
mainfrom
worktree-work-2eoa-seats-aero
May 17, 2026
Merged

Seats.aero as second AwardProvider (work-2eoa)#16
ak2k merged 2 commits into
mainfrom
worktree-work-2eoa-seats-aero

Conversation

@ak2k

@ak2k ak2k commented May 17, 2026

Copy link
Copy Markdown
Owner

Summary

Adds Seats.aero as a second award-data provider alongside PointsPath. First real second consumer of the AwardProvider Protocol that shipped in work-z6zi — validates the abstraction works.

Closes bd `work-2eoa`.

Why both providers

PointsPath Seats.aero
Mileage programs ~17 for Pro tier (US-focused) 25+ including Smiles, Aeroplan, Flying Blue, Qatar, LifeMiles, Virgin Atlantic, etc.
Flight-level detail Yes (carrier × cabin fan-out) Yes (with `include_trips=true`)
Bank transfer-partners ✅ "funded by: Chase, Bilt, Amex"
Per-cabin valuation ✅ `millsToCashRatio` per program ❌ (column renders as "—")
Cost per query N airlines × M cabins parallel requests 1 bulk request

The bank-partner + valuation gaps are intentional — bd work-rq94 + work-eb7k cover those as separate workstreams.

Surface

```bash
flight auth seats key <pro_xxxxxxxx> # save key (0600 perms)
flight auth seats whoami # probe quota
flight auth seats logout # delete file

flight search JFK LHR --dep ... --providers seats # seats only
flight search JFK LHR --dep ... --providers pp,seats # both
flight search JFK LHR --dep ... --provider-opt seats.sources=american,united
```

Key resolution: `SEATS_AERO_API_KEY` env > `~/.config/flight-cli/seats.json` > none. Env path is right for sops/direnv flows; the on-disk path is "save once" convenience.

Implementation

```
src/flight_cli/providers/seats_aero/
auth.py # env > file > None resolver, 0600 save, get/clear
models.py # pydantic for /partnerapi/search (PascalCase mirror)
client.py # async httpx wrapper; X-RateLimit-* tracking; pagination
provider.py # AwardProvider impl; trip → AwardFlight grouping
```

Grouping key for trips → AwardFlights: `(FlightNumbers, DepartsAt, Source)`. Same physical journey priced in Y/W/J/F under the same program collapses into one AwardFlight with multiple CabinAward entries. Different connecting segments (e.g. AA2643→AA730 vs AA2643→AA732) stay distinct.

The PP code path (`run_pp_for_search`) is generalized to gate token validation on whether PP is actually in the filter — so `--providers seats` no longer hard-errors when PP tokens are missing.

Test plan

  • 265 tests pass (33 new: `tests/seats_aero/test_{auth,models,provider}.py`)
  • Basedpyright clean: `0 errors, 0 warnings, 0 notes`
  • Ruff clean
  • Live smoke against real API: `flight search JFK LHR --dep 2026-08-15 --backend gflight --providers seats` joins Flying Blue, Smiles, Qatar, American, Virgin Atlantic awards against the gflight cash table on flight#+date. ¢/mi computed where the cabin matches.
  • Fixtures captured from real responses (5 results with `include_trips=false` and `include_trips=true`) — pinned at `tests/fixtures/seats_aero/` so seats.aero schema drift becomes a loud test failure.

Quota note

Seats.aero Pro tier is 1000 requests/day. `flight auth seats whoami` probes the current remaining count via `X-RateLimit-Remaining`. The provider does one bulk request per leg per search — far fewer than PP's N × M fan-out.

ak2k added 2 commits May 17, 2026 08:49
Adds Seats.aero as a second award-data provider alongside PointsPath.
Validates the work-z6zi AwardProvider abstraction by being its first
real second consumer.

Why Seats.aero alongside PointsPath:
  - Wider mileage-program catalog: American AAdvantage, BA Avios,
    Virgin Atlantic, Air France/KLM Flying Blue, Finnair Plus, Avianca
    LifeMiles, Smiles (GOL), Qatar Privilege Club, etc. — programs that
    PointsPath's enabled-airlines tier doesn't reach.
  - Per-program/per-day availability with optional flight-level detail.
  - 1000 calls/day on Pro tier vs PP's per-airline-per-cabin fan-out;
    one bulk request covers what PP needs N parallel requests for.

What PP still does better: bank transfer-partner metadata ("funded by:
Chase, Bilt, Amex") and per-program miles-to-cash valuations. These are
populated as empty for Seats.aero awards; the columns render as "—".
Filling them in is the work of separate issues (work-rq94 transfer
bonuses, work-eb7k per-cabin ¢/mi).

Surface:
  flight auth seats key <KEY>     # save Pro API key (0600)
  flight auth seats whoami        # probe quota; show X-RateLimit-Remaining
  flight auth seats logout        # delete the file
  flight search ... --providers seats
  flight search ... --providers pp,seats          # both
  flight search ... --provider-opt seats.sources=american,united

Key resolution: SEATS_AERO_API_KEY env > ~/.config/flight-cli/seats.json
> None. The env-var path is the right choice for sops/direnv-driven
secrets workflows; the on-disk path is for "save once" convenience.

Implementation:
  src/flight_cli/providers/seats_aero/
    auth.py      # env > file > None resolver, save/clear/get helpers
    models.py    # pydantic for the /partnerapi/search response
    client.py    # async httpx wrapper; RateLimit tracking; pagination
    provider.py  # AwardProvider impl; groups trips by (FlightNumbers,
                 # DepartsAt, Source) into AwardFlight with multi-cabin

Tests: 33 new (auth precedence + perms; parser against captured live
fixtures at tests/fixtures/seats_aero/; provider grouping + label
mapping). All 265 total pass; basedpyright clean; ruff clean.

Live smoke against real API: JFK→LHR 2026-08-15 with --providers seats
joins Flying Blue, Smiles, Qatar, American, Virgin Atlantic awards
against the gflight cash table on flight#+date.

Stale comment in providers/registry.py about "today: hardcoded
PointsPath entry" is updated.
…, config

Centralized alias map in _config.py:
  pp           ← pp, pointspath, points-path
  seats-aero   ← seats-aero, seatsaero, seats.aero, sa

Normalization happens at three boundaries:
  - --providers CSV parsing (in _resolve_providers)
  - --provider-opt key parsing (in parse_provider_opt_overrides)
  - config.toml [providers.<name>] section lookup (in provider_options and
    _resolve_providers' base_opts merge)

Inside the codebase, only canonical names ("pp", "seats-aero") appear:
  - registry.py:_construct_enabled gates on canonical
  - cli.py:_should_run_awards reads canonical from the resolved filter
  - ProviderSelection.seats_sources() reads provider_opts["seats-aero"]

Auth subcommand registers both names against the same sub-app:
  flight auth seats-aero {key,whoami,logout}    # canonical
  flight auth sa {key,whoami,logout}            # alias
Both `--help` panels show the same three commands; behavior is identical.

The pp/seats-aero asymmetry (one short, one long) is intentional: "pp" is
PointsPath's own short identifier (everyone in points-world uses it), so
the canonical stays "pp" and "pointspath" is the alias. Seats.aero has no
widely-used short form, so the canonical is "seats-aero" and "sa" is the
short alias.

13 new tests across test_config (canonical_provider helper, parse-time
alias normalization) and test_provider_selection / test_registry (--providers
+ --provider-opt + config.toml + _matches with aliases). 278 total pass.
@ak2k ak2k merged commit cce95b7 into main May 17, 2026
2 checks passed
@ak2k ak2k deleted the worktree-work-2eoa-seats-aero branch May 17, 2026 23:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant