Seats.aero as second AwardProvider (work-2eoa)#16
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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
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.