Releases: saraswatayu/swoop
Releases · saraswatayu/swoop
v0.6.0
Added
- Destination discovery API —
swoop.explore(origin, …)andswoop explore <ORIGIN>CLI: swoop's fourth primitive, "where could I go from here?", via theGetExploreDestinationsRPC. ReturnsExploreDestinationobjects (name, country, coordinates, images, Google's suggested dates, and drive time for nearby drivable destinations), one-way or roundtrip. - Explore bridge —
swoop.price_explore(destination)prices a chosen destination's cheapest itinerary via the existing search/price pipeline;swoop.price_explore_all(destinations)prices a whole page of destinations concurrently (order-preserving, capped worker pool;Nonefor unpriceable entries; a rate limit stops the batch and propagates so the caller backs off rather than hammering);ExploreDestination.to_search_kwargs()for direct integration. The Explore RPC returns no price (it is discovery, not pricing), so prices come from composition — mirroringdeals()→price_deal(). The discovery query context — cabin, full passenger party, andmax_stops— is carried onto eachExploreDestination, so pricing re-prices the same product (a nonstop exploration prices a nonstop, not a cheaper connection). - New
ExploreDestinationandExploreResultdataclasses with frozen API-surface coverage. The deals-style client-side filters (destinations,exclude_destinations,region,trip_length) are exposed onexplore()itself — not just the CLI — so library callers can narrow the set too. CLI supports table/JSON/CSV/brief output (CSV neutralizes spreadsheet formula prefixes) and--destination,--exclude-destination,--region,--trip-length, plus--one-way. Stablebl/f.sidsession params are cached across calls (complete params only, fetched single-flight) to skip a redundant page fetch, with self-healing on a stale-session rejection.
v0.5.0
Added
- Deals discovery API —
swoop.deals(origin, …)andswoop deals <ORIGIN>CLI for finding the best flight deals from an airport (or set of airports). Returns up to 30Dealobjects with destination, dates, price, typical price, discount %, carriers, stops, duration, trip length, anddestination_region. - Deal bridges —
swoop.search_deal(deal)resolves a deal to specific itineraries;swoop.price_deal(deal)prices the cheapest matching itinerary.Deal.to_search_kwargs()for direct integration. A discovered deal now flows into swoop's existing search/price pipeline.Dealstores its query context (cabin, passengers) so the bridges forward those correctly. - Filter surface (RPC-native) —
airlines,max_stops,cabin,passengers,include_basic_economy(defaults toFalseto matchsearch()— prevents the basic-economy footgun at $200-to-Lisbon prices). - Filter surface (client-side) —
depart_window,trip_length,destinations,exclude_destinations,region,max_price,min_discount_pct. Applied to the 30 deals the RPC returns; combinable with the native filters. - Multi-origin support —
originacceptsstrorlist[str]. Default single-call mode uses the RPC's slot-0 airport set;per_origin=Trueissues parallel calls (capped at 4 workers, thread-safe_get_clientwith a per-workerprimp.Client), merges byDeal.fingerprint, keeps the cheaper variant on collision. CLI:swoop deals JFK,LGA,EWR [--per-origin].DealsResult.originis comma-joined when multiple origins are passed.DealsResult.partial=Trueis set when at least one parallel call fails so callers can avoid persisting incomplete snapshots. - Region scoping —
swoop.Regionenum (NORTH_AMERICA,CARIBBEAN,LATIN_AMERICA,EUROPE,AFRICA,MIDDLE_EAST,ASIA_PACIFIC) backed byairportsdatafor IATA→country→region.Deal.destination_regionpopulated automatically whenairportsdatais installed. CLI--regionwarns once ifairportsdatais missing. - Deals watcher —
swoop.watch_deals(result, cache_path=…)loads the prior snapshot, diffs, and persists current.swoop.diff_deals(prior, current)is the pure-function diff. NewDealsDiffandPriceChangedataclasses surfacenew,gone,price_changes, andunchanged.Deal.fingerprintprovides stable identity across runs (includes query context, excludes price so drops surface asprice_changes). Snapshot cache is schema v2.examples/deals_watcher.pyis the runnable analogue ofexamples/price_drop_watcher.py. The watcher refuses to overwrite the cache when results are partial or empty, and still surfaces the diff for the successful origins. Deal.airlines: list[str]/Deal.airline_names: list[str]even for single-carrier deals. The*sentinel for multi-airline deals is preserved as a single-element list — callers can detect and route on it.- BookingOption seller fields —
seller_name(e.g."Mytrip","Qatar Airways"),seller_code(e.g."ETRAVELI_Mytrip","QR"),booking_url(thegoogle.com/travel/clk/f?u=…redirect that opens the seller's page),logo_url(gstatic partner logo when Google provideslogo_code), andis_airline_direct(Truewhen the booking is direct with the operating carrier rather than an OTA). Parsed fromGetBookingResults. swoop price --jsonexposes the fullBookingOptionfield set, includingfare_family,rebookability_signal, and all five new seller fields.swoop price --csv— CSV output for price lookups, includinglogo_urland lowercase boolean fields.--verbose/-vCLI flag surfaces RPC debug logging (scoped to the Click invocation, never persists globally).- Auto-detects non-TTY stdout and runs quietly so output pipes cleanly into other tools.
- CSV formula-injection escape — values starting with
=,+,-,@,\t,\rare prefixed with'to neutralize Excel/Sheets formula execution. Applies toswoop search --csv,swoop price --csv, andswoop deals --csv. - CLI validates
--depart-windowand--trip-lengthranges with a clear error on the offending value. swoop --versionnow reportsswoop.__version__(was stale).- Shell completion documented in the README.
- Contract corpus tests assert
seller_code,is_airline_direct, and non-emptybooking_urlfor every captured response, so a future Google reshape fails loudly. - New runnable scripts in
examples/:deals_watcher.py(paired with the existingprice_drop_watcher.pyandmulti_city_finder.py). MIGRATION.mdcovering 0.3 → 0.4 and 0.4 → 0.5.SECURITY.mddocumenting the threat model and disclosure policy.PyPIhomepage points to the landing page; additional project URLs added.
Changed
- One-way pricing now fetches
GetBookingResultsand returns the cheapest eligible booking option price (withbooking_optionspopulated andrpc_callsincremented by 1), instead of short-circuiting to the search-result price. Affectscheck_price,price_selector, andprice_legsfor single-leg trips:PriceResult.pricemay differ fromItinerary.priceandPriceResult.booking_optionsis no longer empty for one-ways. If the booking RPC fails or returns no eligible options, the search-result price is still used as a fallback. See MIGRATION.md for details. BookingOption.__repr__surfaces bothseller_nameandbrand_labeltogether when both are present, and usesrepr()on each so apostrophes and quotes no longer corrupt log output._extract_sellerextracts the click URL (opt[5]) independently of the seller block (opt[1]); a missing seller no longer drops the booking URL.logo_urlis empty when Google omitslogo_code. The previous behaviour silently constructed a logo URL fromseller_codewhich 404s for OTA codes; callers that want the airline-direct fallback can construct it themselves via{gstatic_base}/{seller_code}.png.- Internal symbols are hidden from the public surface via PEP 562
__dir__instead ofglobals().pop; tab completion anddir(swoop)now show only the documented API.
Fixed
region_for_iatano longer spends ~37 ms/call —airportsdata.load()is now cached. Material speedup for batch deal annotation._deals_date_rangeno longer crashes on malformed input or on Windows._dict_to_dealhardened against null fields and string-bool coercion from older cache snapshots.filter_dealsraisesValueErroron a malformeddepart_windowrather than silently returning empty.Deal.stopsisOptional[int]; the CLI renders unknown as?instead of crashing.DealsDiffandPriceChangeare no longerfrozen=True— the prior config produced a broken hash.- Detect HTML/CAPTCHA responses in the deals streaming parser and raise a typed error instead of a confusing decode crash.
- Validate
booking_pathprefix before constructing booking URLs. - One-way booking-RPC failures are caught at
SwoopError(broadened from a narrower class) so the search-price fallback always engages. format_deals_jsonexposes thepartialflag and per-Dealquery context in the JSON output.examples/deals_watcher.py --onceskips the rate-limit sleep and guards against missingairportsdata.examples/price_drop_watcher.py --onceexits non-zero on fetch failure and is hardened against partial args and torn writes.- CLI verbose-logging lifetime is scoped to the Click context (no global handler leakage in long-running hosts).
Notes (deals upstream behavior, verified by live probe)
- Deals discovery is roundtrip-only —
trip_type=2(oneway) is ignored by the upstream RPC; the server returns roundtrip-shaped deals regardless. swoop does not expose atrip_typeparameter ondeals(). For one-way exploration, usesearch()with an explicit destination. - The RPC ignores the dates passed in the payload and returns its own forward window (~4 months).
depart_windowis enforced client-side. - The RPC ignores time-of-day restrictions. Time-window filters are not exposed on
deals().
v0.4.1
Added
impersonatefield onTransportConfigfor TLS fingerprint rotation (e.g."chrome","firefox","safari","edge")
Full Changelog: swoop-v0.4.0...swoop-v0.4.1
v0.4.0
Added
- Multi-currency support — prices display in the correct currency based on point-of-sale country, with locale-aware formatting via Babel (
$1,234,£890,¥5,540) currencyfield onTripOption,PriceResult, andSearchResult(derived property)--countryflag (orset_country()) for point-of-sale control — affects fares and currency returned by Google--proxyflag (orset_proxy()) for routing requests through HTTP/SOCKS5 proxies--children,--infants-in-seat,--infants-on-lapCLI flags for full passenger breakdownPassengersdataclass consolidating adult/child/infant countsTransportConfigdataclass consolidating timeout, retries, country, and proxy settings--max-results,--beam-width,--time-budgetCLI flags for configurable multi-city beam search- CO₂ emissions column in search table — color-coded percentage vs average
- Legroom display for nonstop single-segment flights
- Overnight indicators on layovers
- Airline names in search table with multi-leg labeling
CabinClassLiteral type ("economy" | "premium-economy" | "business" | "first") replacing stringly-typed cabin validationSegment.legroom,Segment.has_premium_ife,Segment.amenities,Segment.seat_typefieldsItinerary.stop_count,Itinerary.is_budget_carrier,Itinerary.quality_signalsfieldsBookingOption.fare_familyandBookingOption.rebookability_signalfields- Human-readable
__repr__on all public dataclasses (Segment,Itinerary,TripOption,SearchResult,PriceResult,ResolvedLeg,BookingOption,Layover,TripLeg) - HTTP connection reuse across requests for keep-alive
- LRU-evicted client cache (max 32) for proxy rotation without unbounded memory growth
Fixed
- Cabin class misidentification — replaced fragile brand-name text matching with numeric field
brand_block[6][0][0]; airlines like British Airways ("Upper Class" for business) and Turkish ("Premium Flex") were being silently misclassified - OTA/codeshare booking options dropped — third-party sellers and codeshare flights with null brand fields were rejected entirely, causing zero booking options on some routes (e.g. SFO→NRT via Philippine Airlines)
- Currency-unaware price division — hardcoded
/100divisor was wrong for JPY, INR, KRW, and other whole-unit currencies - Roundtrip beam search overhead — roundtrips were running 16 RPC calls via beam search instead of 1; fast path now covers 1- and 2-leg trips, beam search only triggers for 3+ legs
- Midnight departure times — Google encodes midnight as
hour=None; now treated as 0 - Missing itinerary-level times — falls back to first/last segment departure/arrival when itinerary-level times are absent
- Single-element time tuples — departure times arriving as
[hour]without minutes now default minute to 0 exclude_basic_economynot propagated — flag was only sent on single-leg first-pass RPC; now sent to all RPC stages for roundtrip and multi-city- Proxy/children/infants silently dropped — multiple internal functions accepted these params but didn't forward them to lower-level calls
- Currency field lost on flight-number filter —
_filter_trip_options_by_flight_number()dropped currency when constructing filtered results - Unbounded client cache — proxy rotation caused unlimited memory growth; now LRU-evicted at 32 entries
Changed
- Breaking:
Flightclass renamed toSegment—Itinerary.segmentsnow containsSegmentobjects - Breaking:
BookingOptiondict-style access removed (option['price']→option.price);__getitem__,get(),keys(),values(),items()all removed - Breaking:
search(),check_price(), and related functions now acceptTransportConfigandPassengersdataclass objects instead of scattered kwargs SearchResult.currencyis now a derived property (computed from first result) instead of a stored field- Search table redesigned with columnar layout for better multi-leg readability
- CLI options reordered by frequency of use — trip basics first, advanced/transport options last
- Renamed internal
_centsfields/functions to_rawfor clarity - Eliminated
_build_filters/_build_f_reqredundancy inrpc.py - Removed backward-compat
SearchResultalias fromdecoder.py - Removed dead
correct_trip_option_prices()function - Single source of truth for
CABIN_CLASS_MAPinbuilders.py - Extracted shared formatting helpers to
_formatting.py
swoop-v0.3.1
What's Changed
- feat: add check_price() for targeted flight price lookups by @saraswatayu in #2
- feat: CLI redesign with leg-based core and flight_summary by @saraswatayu in #3
- feat: multi-city staged search and CLI selectors by @saraswatayu in #4
- v0.3.0 by @saraswatayu in #5
- test: improve test suite coverage and CI automation by @saraswatayu in #6
New Contributors
- @saraswatayu made their first contribution in #2
Full Changelog: https://github.com/saraswatayu/swoop/commits/swoop-v0.3.1