Add PointsPath integration: award prices joined to Matrix cash itineraries#1
Merged
Conversation
…aries New `--pp` flag on `flight fare` augments the cash table with award prices fetched from PointsPath's per-airline `/api/airline-search` endpoint, joined client-side on (flight number, departure date). Funding banks come from `/api/pricing-info` (cached 24h). Computes ¢/mi against cash prices. Auth lives under a new `flight auth pp` subgroup (login / whoami / logout) to leave room for additional providers later. Today only `--tokens-file` import works; browser-based login deferred to a follow-up. Deferred: calendar mode, multi-leg / round-trip PP queries, tests, README.
* Round-trip / multi-city: per-leg PP queries via new LegQuery, one table per leg labeled outbound/return/leg N. Matcher takes slice_index so the return leg matches against Itinerary.slices[1] etc. Added per-leg dedupe so Matrix's NxN itinerary cross-product collapses to one row per unique (flight#, departure_date), keeping the cheapest cash pairing. * Dynamic airline catalog: GET /api/extension-config (cached 7d) feature flags intersected with /api/pricing-info airline universe. Always-on airlines (no enable<X> flag) included; explicitly disabled (enable<X>=0) excluded; versioned flags (enableAirFranceV2) handled via regex. --pp-airlines still overrides for explicit selection. Static DEFAULT_AIRLINES kept as last-resort fallback if discovery fails. * Tests: 60 pytest cases covering null-coercion in models, key normalization + outer-join semantics in matcher, currency-string parsing variants, airline-flag pattern matching. Fixtures are tiny hand-curated JSON; no live API hits. * README: PointsPath section documenting --pp / --pp-only / --pp-airlines / --pp-cabin flags, auth setup, airline-discovery rule, and what's deferred.
After rebasing onto main's adopted conventions, the pp/* and tests/pp/*
code needed a localized cleanup pass to satisfy the strict ruleset.
All behavior-preserving:
* Logging: stdlib `logging.getLogger` → `structlog.get_logger` typed as
BoundLogger via TYPE_CHECKING. Printf-style log calls rewritten as
key-value events (e.g. `log.warning("pp_airline_search_failed",
airline=..., status=..., body=...)`).
* Async: asyncio.Semaphore → anyio.Semaphore; asyncio.gather → anyio
TaskGroup (cleaner per-task error capture); asyncio.run → anyio.run
via a closure for kwargs-bearing _gather_pp call.
* HTTP boundaries: bare 200/204/400/401 → http.HTTPStatus.* enum
constants.
* Pydantic config: pp.models._Loose flipped extra="allow" →
extra="ignore" to match the main models._Loose Profile-B-edge
convention (PointsPath is reverse-engineered, same justification as
Matrix responses).
* pp/models.py added to pyproject.toml's N815 per-file-ignore — mirrors
PointsPath JSON shape with camelCase attrs (same shape as wire.py).
* Imports: app-local types used only in annotations moved to
TYPE_CHECKING in match.py / client.py / cli.py. Exception: `Path`
stays at runtime in pp/cli.py — typer evaluates Annotated[Path|None,
typer.Option(...)] at runtime, so TYPE_CHECKING breaks the CLI.
* Annotations: bare `dict` → `dict[str, Any]` via _JsonDict aliases;
added types to test parametrize fixtures; B904 `from e` on re-raises;
HTTPStatus import.
* Strict shims: `# pyright: reportPrivateUsage=false` on tests/pp/
test_cli.py (intentional access to _normalize_cabin/_parse_cash/
_parse_csv); `# pyright: reportCallIssue=false` on tests/pp/
test_match.py (pydantic Field-with-alias confuses the constructor-
kwargs check despite populate_by_name=True).
* cli.py: reuses the existing _ROUND_TRIP_LEGS constant; lazy fli/
fast_flights imports kept.
* Unicode: ambiguous `×` replaced with `x` in docstrings + titles to
satisfy RUF001/RUF002 (consistent with the Carrier-x-stops convention
PR #2 established).
`make check`: 0 errors / 0 warnings / 69 tests passing. Live `flight
fare JFK LHR` smoke test green (10 solutions, anyio runtime,
ConsoleRenderer log output on stderr).
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 a new
--ppflag onflight farethat fetches PointsPath award prices for the same route and joins them onto the Matrix cash itineraries client-side, plus a smallflight auth ppsubgroup for token management.The AA106 vs BA178 contrast is the value-add: same airline-of-record award, but the BA-operated leg pulls $308 in fuel surcharges, dropping value from 2.2¢ to 1.6¢/mi.
Architecture
New
src/flight_cli/pp/package:models.pyairline-search+pricing-info(with null→[] coercion)auth.py~/.config/flight-cli/pp.json, Supabase JWT refresh, JWT decode helpersclient.pyhttpxclient; semaphore-bounded fan-out (default 5); 401→refresh→retry; 24h on-disk cache forpricing-infomatch.py(flight_number, departure_date)cli.pyauth ppTyper subapp (login/whoami/logout) +run_pp_for_searchaugmenter + Rich renderEdits to existing
src/flight_cli/cli.pyare minimal: register theauthsubapp, add 4 flags tofare, and call the augmenter when--ppis set. Existing commands are untouched.API surface (reverse-engineered from extension traffic)
POST /api/airline-search— one POST per airline; returnsoutboundFlights[]withperCabinMilesPricingper flightGET /api/pricing-info— airline → transfer-partner banks + active bonus ratios + expiryAuthorization: Bearer ...; refresh viahttps://hxjqzkcirzhjvtubefie.supabase.co/auth/v1/tokenPP_SUPABASE_ANON_KEYenv var overridesAuth
Browser-based login is deferred — needs Playwright + the CDP cookie-sniff flow we used to bootstrap. For now, capture tokens from a logged-in Chrome session and pass them as JSON.
--helpdocuments this.Deferred (intentionally not in this PR)
auth pp login(Playwright integration)calendar --pp(fan-out is N days × M airlines; deserves its own design pass)Test plan
flight auth pp login --tokens-file /tmp/pp_tokens.json✓flight auth pp whoamidecodes JWT, prints email + sub + expiry ✓flight auth pp logoutremoves the store file ✓flight fare JFK LHR --dep 2026-06-09 --pp --pp-airlines United,Delta,American→ joined table with correct ¢/mi (verified 2.2¢ for AA award, 1.6¢ for AA-via-BA codeshare with surcharges) ✓pricing-infocache populated at~/.cache/flight-cli/pp_pricing.json, second invocation hits cache ✓--ppis not used--return— currently uses leg 0 dep_date only; return leg ignored