Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/flight-search/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ intent into the right invocation **on the first try**.

Global flags (every search-printing command):
- `-v` / `-vv` — verbose logging (cache hits, retries) to stderr
- `--format envelope` — `search` and `calendar`: one JSON object with the same keys whichever path answered (`version`, `command`, `backend`, `currency`, `complete`, `notes`, `results`, `awards`, `insight`, `price_history`, `facets`, `price_graph`, `verify`, `cross_check`, `split_ticket`). It is written at exit 0 and 1; a usage error (exit 2) writes no document. Use it whenever the output is read by a program: `complete: false` means the answer is narrower than asked (on a calendar, also departure dates Matrix priced no fare on, each named in `notes`), and `notes` says why. A calendar's result rows carry their `departure` and `return` dates beside `row`. On a Google search, `facets` lists what Google's filters offer (fare, trip-length and layover ranges, airlines, alliances, connecting airports): read it to choose a carrier, alliance, `--max-price`, `MAXDUR` or `MAXCONNECT` before a second search. Add `--gf-transport browser` or `auto` to a calendar to read Google's price graph into `price_graph` (opens a headless Chrome); without either the key is empty and its note says so
- `--format envelope` — `search` and `calendar`: one JSON object with the same keys whichever path answered (`version`, `command`, `backend`, `currency`, `complete`, `notes`, `results`, `awards`, `insight`, `price_history`, `facets`, `price_graph`, `verify`, `cross_check`, `split_ticket`). It is written at exit 0 and 1; exit 2 (a usage or configuration error) writes no document. Use it whenever the output is read by a program: `complete: false` means the answer is narrower than asked (on a calendar, also departure dates Matrix priced no fare on, each named in `notes`), and `notes` says why. A calendar's result rows carry their `departure` and `return` dates beside `row`. On a Google search, `facets` lists what Google's filters offer (fare, trip-length and layover ranges, airlines, alliances, connecting airports): read it to choose a carrier, alliance, `--max-price`, `MAXDUR` or `MAXCONNECT` before a second search. Add `--gf-transport browser` or `auto` to a calendar to read Google's price graph into `price_graph` (opens a headless Chrome); without either the key is empty and its note says so
- `--format csv` (`search`) — a header row, then one row per itinerary the table lists, for a spreadsheet or a script that reads no JSON: idx (the table's row number), total_price (a plain decimal with no currency code; for a party, the party's total, empty where Matrix states none), currency, num_stops (summed over the slices), total_duration_min (the slices' durations summed), carriers (each code once, in flight order, joined by `;`), flight_numbers (every flight in slice order, joined by `;`), origin and destination (the first slice's), dep_date (the first slice's local departure date, YYYY-MM-DD). Cash fares only: no award provider is asked, and `--awards-only`, several `--cabin` values, `--slice`, `--split`, `--sellers`, `--fare-rules`, `--verify` and `--enrich` are usage errors (exit 2). An answer with no rows writes the header alone; a failed search writes nothing. `calendar` and `detail` refuse it
- `--format json` — the answering path's own shape (Google Flights rows, Matrix's raw response, `{cabin: …}`, or the award document; a calendar under `--gf-transport browser` or `auto` adds `google_price_graph` to Matrix's body); `--json` is a deprecated alias for it
- Several cabins (`--cabin economy,business`) — one table, a row per itinerary and a price column per cabin, sorted by `--sort` (default the first cabin). Every cabin is priced on the sort cabin's itineraries, so another cabin's own cheapest can be on no row; a line under the table then names it, e.g. `J's own cheapest: USD2508.00 (UA2831+UA1509 / UA373+UA2282), on no row above; --sort business lists J's cheapest first.` For a party it is priced as the table's cells are and says so: `J's own cheapest, total for 2 travelers: …`, or `, per traveler: …` where Matrix states no total. On Google, `--format json` and `--format envelope` list each cabin's `-n` cheapest rows, then every other row whose fare the table or the line under it prints in that cabin, in price order; on Matrix, each cabin's whole answer
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Every result-printing command supports:
- `--fare-rules` (`search`) — after the table, print itinerary `--pick N`'s fare basis, booking codes and fare rules (penalties, changes, refunds) from Matrix
- `--verify` (`search`, Google Flights) — after the table, ask Matrix for itinerary `--pick N` as exactly that itinerary (its flights by number, each on its own day and minute, between its airports) and print Matrix's price beside Google's with the fare basis, booking codes and fare rules; or say why Matrix does not price it: those flights only on another itinerary, no fare, or that none of the trips Matrix returned for that route and day names its carrier. For a party, Matrix's price is the party's total, as Google's is, or one passenger's marked per traveler where Matrix states no total. With `--format json` the document is `{"search": [...], "verify": {...}}`, and `--format envelope` carries the same object under `verify`; `verify.matrix.price` and `verify.delta` (Google's price minus Matrix's) are the party's, and `verify.matrix.per_traveler` is one passenger's
- `--no-separate-tickets` (`search`, Google Flights) — hide the itineraries Google sells as separate tickets, which every search that shows Google rows otherwise adds from Google's Cheapest tab, marked `†` (`‡` for a self transfer, where bags are rechecked between flights) on the Google, merged and multi-cabin tables or `separate_tickets: true`, a round trip as its outbound alone at Google's round-trip total, never priced against Matrix (cross-check reason `separate_tickets`) and skipped, with a reason, by awards, `--sellers`, `--verify` and pinned links; on a multi-city `--slice` search, no one-way tickets are asked for
- `--format envelope` (`search`, `calendar`) — for agents and scripts: one JSON object with the same keys whichever path answered (`version`, `command`, `backend`, `currency`, `complete`, `notes`, `results`, `awards`, `insight`, `price_history`, `facets`, `price_graph`, `verify`, `cross_check`, `split_ticket`). It is written at exit 0 and 1; a usage error (exit 2) writes no document. `complete` is false when the answer is narrower than asked, such as a cabin, a calendar sub-query, a departure date Matrix priced no fare on, an award provider or Google's Cheapest tab lost, and `notes` carries what stderr said. A calendar's result rows carry their `departure` and `return` dates beside `row`. A Google search's `facets` are what Google's filters offer for it (fare, trip-length and layover ranges, airlines, alliances, connecting airports), to choose a carrier, alliance, `--max-price`, `MAXDUR` or `MAXCONNECT` without a second search. A calendar's `price_graph` is Google's estimate per date pair, read under `--gf-transport browser` or `auto`. Schema: [`docs/envelope.schema.json`](docs/envelope.schema.json)
- `--format envelope` (`search`, `calendar`) — for agents and scripts: one JSON object with the same keys whichever path answered (`version`, `command`, `backend`, `currency`, `complete`, `notes`, `results`, `awards`, `insight`, `price_history`, `facets`, `price_graph`, `verify`, `cross_check`, `split_ticket`). It is written at exit 0 and 1; exit 2 (a usage or configuration error) writes no document. `complete` is false when the answer is narrower than asked, such as a cabin, a calendar sub-query, a departure date Matrix priced no fare on, an award provider or Google's Cheapest tab lost, and `notes` carries what stderr said. A calendar's result rows carry their `departure` and `return` dates beside `row`. A Google search's `facets` are what Google's filters offer for it (fare, trip-length and layover ranges, airlines, alliances, connecting airports), to choose a carrier, alliance, `--max-price`, `MAXDUR` or `MAXCONNECT` without a second search. A calendar's `price_graph` is Google's estimate per date pair, read under `--gf-transport browser` or `auto`. Schema: [`docs/envelope.schema.json`](docs/envelope.schema.json)
- `--format csv` (`search`) — for a spreadsheet or a script that reads no JSON: a header row, then one row per itinerary the table lists. Columns: idx (the table's row number), total_price (a plain decimal with no currency code; for a party, the party's total, empty where Matrix states none), currency, num_stops (summed over the slices), total_duration_min (the slices' durations summed), carriers (each code once, in flight order, joined by `;`), flight_numbers (every flight in slice order, joined by `;`), origin and destination (the first slice's), dep_date (the first slice's local departure date, YYYY-MM-DD). The search takes the path `--format json` takes, and no link line or note goes to stdout. Cash fares only: no award provider is asked, and `--awards-only`, several `--cabin` values, `--slice`, `--split`, `--sellers`, `--fare-rules`, `--verify` and `--enrich` are usage errors (exit 2). An answer with no rows writes the header alone; a failed search writes nothing. `calendar` and `detail` refuse it
- `--format json` — the answering path's own document: Google Flights rows, Matrix's raw response (a calendar's with `google_price_graph` beside it under `--gf-transport browser` or `auto`), `{cabin: …}` for several cabins, or the award document when awards run. `--json` is a deprecated alias for it
- `--no-cache` — bypass the on-disk response cache (`~/.cache/flight-cli/`)
Expand Down
2 changes: 1 addition & 1 deletion docs/memories/envelope.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ with no such trip length. A Google cell's are its own
`departure` and `return`.

Exit codes are those of `--format json` in the same state, and an envelope is
written at exit 0 and 1. Exit 2 is a usage error and writes none.
written at exit 0 and 1. Exit 2 is a usage or configuration error and writes none.

## What makes `complete` false

Expand Down
2 changes: 1 addition & 1 deletion src/flight_cli/_envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def run(
) -> None:
"""Run `call` as an envelope run, then write its one envelope to stdout.

No envelope at exit 2: that is a usage error, and the command never ran.
No envelope at exit 2: that is a usage or configuration error, and the command never ran.
With `csv_out` the run writes the recorded table as CSV instead, at exit 0
alone: a failed run writes nothing, so a header never stands for an answer.
`consoles` print one stderr line per message for the run, so each message
Expand Down
3 changes: 2 additions & 1 deletion src/flight_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11501,7 +11501,8 @@ def _resolve_gf_transport(mode: str) -> GfTransportMode:
_ENVELOPE_FORMAT_CHOICES = "/".join(_ENVELOPE_FORMATS)
_ENVELOPE_FORMAT_HELP = (
"envelope is one versioned "
"JSON document, written at exit 0 and 1; a usage error (exit 2) writes no document. "
"JSON document, written at exit 0 and 1; exit 2 (a usage or configuration error) "
"writes no document. "
"Its keys, whichever path answered: version, command, backend, currency, complete, "
"notes, results, awards, insight, price_history, facets, price_graph, verify, "
"cross_check, split_ticket. complete is false when the answer is narrower than "
Expand Down
16 changes: 12 additions & 4 deletions tests/test_envelope_usage_error_docs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""`--format envelope` is written at exit 0 and 1; a usage error (exit 2) writes no document."""
"""`--format envelope` is written at exit 0 and 1; exit 2 (usage or configuration) writes none."""

from __future__ import annotations

Expand All @@ -10,7 +10,7 @@
from flight_cli import cli

_ROOT = Path(__file__).resolve().parent.parent
_SAYS = "a usage error (exit 2) writes no document"
_SAYS = "exit 2 (a usage or configuration error) writes no document"
_BULLET_DOCS = [
_ROOT / "README.md",
_ROOT / ".claude" / "skills" / "flight-search" / "SKILL.md",
Expand All @@ -21,7 +21,7 @@ def _flat(text: str) -> str:
return " ".join(text.replace("│", " ").split())


def test_the_format_help_says_a_usage_error_writes_no_document() -> None:
def test_the_format_help_says_exit_2_writes_no_document() -> None:
for command in ("search", "calendar"):
result = CliRunner().invoke(cli.app, [command, "--help"], env={"COLUMNS": "200"})
flat = _flat(result.stdout)
Expand All @@ -31,7 +31,7 @@ def test_the_format_help_says_a_usage_error_writes_no_document() -> None:


@pytest.mark.parametrize("doc", _BULLET_DOCS, ids=lambda p: p.name)
def test_the_envelope_bullet_says_a_usage_error_writes_no_document(doc: Path) -> None:
def test_the_envelope_bullet_says_exit_2_writes_no_document(doc: Path) -> None:
(bullet,) = (
ln for ln in doc.read_text().splitlines() if ln.startswith("- `--format envelope`")
)
Expand All @@ -45,3 +45,11 @@ def test_the_envelope_note_title_names_the_exit_codes_that_write_one() -> None:

assert "at exit 0 and 1" in title, title
assert "on every path" not in title, title


def test_the_envelope_note_and_the_run_docstring_name_both_causes_of_exit_2() -> None:
note = _flat((_ROOT / "docs" / "memories" / "envelope.md").read_text())
run_doc = _flat((_ROOT / "src" / "flight_cli" / "_envelope.py").read_text())

assert "Exit 2 is a usage or configuration error and writes none." in note, note
assert "that is a usage or configuration error, and the command never ran." in run_doc
Loading