swoop-flights 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .eggs/
9
+
10
+ # Hypothesis
11
+ .hypothesis/
12
+
13
+ # Test / coverage
14
+ .pytest_cache/
15
+ htmlcov/
16
+ .coverage
17
+
18
+ # Environment
19
+ .env
20
+ .env.*
21
+
22
+ # IDE
23
+ .vscode/
24
+ .idea/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ayush Saraswat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,249 @@
1
+ Metadata-Version: 2.4
2
+ Name: swoop-flights
3
+ Version: 0.1.0
4
+ Summary: Google Flights price scraper. Search flights programmatically via the same RPC endpoints the web app uses.
5
+ Project-URL: Homepage, https://github.com/saraswatayu/swoop
6
+ Project-URL: Issues, https://github.com/saraswatayu/swoop/issues
7
+ Author: Ayush Saraswat
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: flights,google-flights,prices,scraper,travel
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: primp>=1.0.0
23
+ Requires-Dist: protobuf>=4.0.0
24
+ Provides-Extra: validation
25
+ Requires-Dist: airportsdata>=1.3.0; extra == 'validation'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # swoop
29
+
30
+ Google Flights price scraper. Search flights programmatically using the same RPC endpoints the Google Flights web app uses.
31
+
32
+ ```python
33
+ from swoop import search
34
+
35
+ results = search("JFK", "LAX", "2026-06-01")
36
+ for flight in results.best:
37
+ airline = ", ".join(flight.airline_names)
38
+ print(f"${flight.price} — {airline}")
39
+ ```
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install swoop-flights
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ### One-way search
50
+
51
+ ```python
52
+ from swoop import search
53
+
54
+ results = search("SFO", "JFK", "2026-06-15")
55
+
56
+ # results.best — top-ranked flights
57
+ # results.other — remaining flights
58
+ for flight in results.best:
59
+ print(f"${flight.price}")
60
+ print(f" {flight.departure_airport} → {flight.arrival_airport}")
61
+ print(f" {flight.airline_names}, {flight.stop_count} stops")
62
+ print(f" {flight.travel_time} min total")
63
+ ```
64
+
65
+ ### Roundtrip search
66
+
67
+ ```python
68
+ results = search("SFO", "JFK", "2026-06-15", return_date="2026-06-22")
69
+ # Price in results is the roundtrip total
70
+ ```
71
+
72
+ ### Cabin class and filters
73
+
74
+ ```python
75
+ from swoop import search, SORT_CHEAPEST
76
+
77
+ results = search(
78
+ "LAX", "NRT", "2026-06-15",
79
+ cabin="business", # economy, premium-economy, business, first
80
+ max_stops=0, # nonstop only
81
+ sort=SORT_CHEAPEST, # cheapest first
82
+ airlines=["NH", "JL"], # filter to specific carriers
83
+ )
84
+ ```
85
+
86
+ ### Time window filtering
87
+
88
+ ```python
89
+ results = search(
90
+ "JFK", "LHR", "2026-06-15",
91
+ earliest_departure=8, # depart after 8am
92
+ latest_departure=14, # depart before 2pm
93
+ )
94
+ ```
95
+
96
+ ### Booking details (fare options)
97
+
98
+ ```python
99
+ from swoop import search, get_booking_results
100
+
101
+ results = search("JFK", "LAX", "2026-06-15")
102
+ itinerary = results.best[0]
103
+
104
+ # Get fare tiers — just pass the itinerary
105
+ options = get_booking_results(itinerary)
106
+
107
+ for opt in options:
108
+ print(f"${opt.price} — {opt.brand_label} ({opt.fare_family})")
109
+ ```
110
+
111
+ You can also pass a booking token string with explicit parameters:
112
+
113
+ ```python
114
+ options = get_booking_results(
115
+ itinerary.booking_token,
116
+ origin="JFK",
117
+ destination="LAX",
118
+ date="2026-06-15",
119
+ selected_legs=[
120
+ [
121
+ flight.departure_airport,
122
+ f"{flight.departure_date[0]}-{flight.departure_date[1]:02d}-{flight.departure_date[2]:02d}",
123
+ flight.arrival_airport,
124
+ None,
125
+ flight.airline,
126
+ flight.flight_number,
127
+ ]
128
+ for flight in itinerary.flights
129
+ ],
130
+ )
131
+ ```
132
+
133
+ ### Retry and timeout
134
+
135
+ ```python
136
+ # Retry up to 3 times on HTTP 429 (rate limit) with exponential backoff
137
+ results = search("JFK", "LAX", "2026-06-15", retries=3, timeout=90)
138
+
139
+ # Same for booking results
140
+ options = get_booking_results(itinerary, retries=2, timeout=60)
141
+ ```
142
+
143
+ ### Flight details
144
+
145
+ Each `Itinerary` contains detailed segment data:
146
+
147
+ ```python
148
+ results = search("JFK", "LAX", "2026-06-15")
149
+ for itinerary in results.best:
150
+ for flight in itinerary.flights:
151
+ print(f"{flight.airline} {flight.flight_number}")
152
+ print(f" {flight.departure_airport} → {flight.arrival_airport}")
153
+ print(f" Aircraft: {flight.aircraft}")
154
+ print(f" Legroom: {flight.legroom}")
155
+ if flight.co2_grams:
156
+ print(f" CO₂: {flight.co2_grams}g")
157
+
158
+ if itinerary.carbon_emissions:
159
+ ce = itinerary.carbon_emissions
160
+ print(f" Route emissions: {ce.difference_percent}% vs typical")
161
+
162
+ if results.price_range:
163
+ print(f" Price range: ${results.price_range.low}–${results.price_range.high}")
164
+ ```
165
+
166
+ ## Error handling
167
+
168
+ ```python
169
+ from swoop import search, SwoopHTTPError, SwoopRateLimitError, SwoopParseError
170
+
171
+ try:
172
+ results = search("JFK", "LAX", "2026-06-15")
173
+ except SwoopRateLimitError:
174
+ print("Rate limited — wait a few minutes")
175
+ except SwoopHTTPError as e:
176
+ print(f"HTTP {e.status_code}")
177
+ except SwoopParseError as e:
178
+ print(f"Parse error: {e}")
179
+ ```
180
+
181
+ ## API reference
182
+
183
+ ### `search(origin, destination, date, **kwargs)`
184
+
185
+ Search Google Flights and return a `SearchResult`.
186
+
187
+ | Parameter | Type | Default | Description |
188
+ |-----------|------|---------|-------------|
189
+ | `origin` | `str` | required | Origin IATA code |
190
+ | `destination` | `str` | required | Destination IATA code |
191
+ | `date` | `str` | required | Departure date (`YYYY-MM-DD`) |
192
+ | `return_date` | `str \| None` | `None` | Return date for roundtrip |
193
+ | `cabin` | `str` | `"economy"` | `economy`, `premium-economy`, `business`, `first` |
194
+ | `adults` | `int` | `1` | Number of adults |
195
+ | `max_stops` | `int \| None` | `None` | `None`=any, `0`=nonstop, `1`=1 stop, `2`=2 stops |
196
+ | `sort` | `int` | `SORT_DEPARTURE_TIME` | Sort order constant |
197
+ | `airlines` | `list[str] \| None` | `None` | Filter by airline codes |
198
+ | `include_basic_economy` | `bool` | `False` | Include basic economy fares |
199
+ | `timeout` | `int` | `90` | HTTP timeout in seconds |
200
+ | `retries` | `int` | `0` | Retries on HTTP 429 with exponential backoff |
201
+
202
+ Returns `SearchResult | None`. `None` means no results found.
203
+
204
+ ### `get_booking_results(itinerary_or_token, **kwargs)`
205
+
206
+ Get fare options for a specific itinerary. Pass an `Itinerary` object directly, or a booking token string with explicit `origin`, `destination`, `date`, and `selected_legs`. Returns `list[BookingOption]` with `price`, `brand_label`, `brand_code`, `fare_family`, etc. `BookingOption` supports both attribute access (`opt.price`) and dict-style access (`opt["price"]`, `opt.get("price")`).
207
+
208
+ ### Result types
209
+
210
+ - **`SearchResult`** — `best: list[Itinerary]`, `other: list[Itinerary]`, `price_range: PriceRange | None`
211
+ - **`Itinerary`** — Full itinerary with `price`, `flights`, `layovers`, `travel_time`, `booking_token`, `carbon_emissions`
212
+ - **`Flight`** — Segment details: `airline`, `flight_number`, `aircraft`, `legroom`, `co2_grams`, `amenities`
213
+ - **`Layover`** — Stop info: `minutes`, airports, `is_overnight`
214
+ - **`CarbonEmissions`** — `this_flight_grams`, `typical_for_route_grams`, `difference_percent`
215
+
216
+ ### Constants
217
+
218
+ | Constant | Value | Description |
219
+ |----------|-------|-------------|
220
+ | `SORT_TOP` | `1` | Google's default ranking |
221
+ | `SORT_CHEAPEST` | `2` | Cheapest first |
222
+ | `SORT_DEPARTURE_TIME` | `3` | By departure time |
223
+ | `SORT_ARRIVAL_TIME` | `4` | By arrival time |
224
+ | `SORT_DURATION` | `5` | Shortest first |
225
+
226
+ ## Pricing notes
227
+
228
+ Use `itinerary.price` to get the USD price as an integer.
229
+
230
+ By default, `search()` excludes basic economy fares so prices reflect Main Cabin. Pass `include_basic_economy=True` to include them:
231
+
232
+ ```python
233
+ results = search("JFK", "LAX", "2026-06-15", include_basic_economy=True)
234
+ ```
235
+
236
+ ## How it works
237
+
238
+ Swoop uses Google Flights' internal `GetShoppingResults` and `GetBookingResults` RPC endpoints — the same ones the web app calls when you search for flights. Requests are serialized as nested JSON payloads and sent via HTTP POST with browser impersonation (via [primp](https://github.com/deedy5/primp)).
239
+
240
+ Responses are decoded from nested list structures into typed Python dataclasses.
241
+
242
+ ## Dependencies
243
+
244
+ - **[primp](https://github.com/deedy5/primp)** — HTTP client with browser TLS impersonation
245
+ - **[protobuf](https://pypi.org/project/protobuf/)** — Protocol buffer serialization
246
+
247
+ ## License
248
+
249
+ MIT
@@ -0,0 +1,222 @@
1
+ # swoop
2
+
3
+ Google Flights price scraper. Search flights programmatically using the same RPC endpoints the Google Flights web app uses.
4
+
5
+ ```python
6
+ from swoop import search
7
+
8
+ results = search("JFK", "LAX", "2026-06-01")
9
+ for flight in results.best:
10
+ airline = ", ".join(flight.airline_names)
11
+ print(f"${flight.price} — {airline}")
12
+ ```
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install swoop-flights
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### One-way search
23
+
24
+ ```python
25
+ from swoop import search
26
+
27
+ results = search("SFO", "JFK", "2026-06-15")
28
+
29
+ # results.best — top-ranked flights
30
+ # results.other — remaining flights
31
+ for flight in results.best:
32
+ print(f"${flight.price}")
33
+ print(f" {flight.departure_airport} → {flight.arrival_airport}")
34
+ print(f" {flight.airline_names}, {flight.stop_count} stops")
35
+ print(f" {flight.travel_time} min total")
36
+ ```
37
+
38
+ ### Roundtrip search
39
+
40
+ ```python
41
+ results = search("SFO", "JFK", "2026-06-15", return_date="2026-06-22")
42
+ # Price in results is the roundtrip total
43
+ ```
44
+
45
+ ### Cabin class and filters
46
+
47
+ ```python
48
+ from swoop import search, SORT_CHEAPEST
49
+
50
+ results = search(
51
+ "LAX", "NRT", "2026-06-15",
52
+ cabin="business", # economy, premium-economy, business, first
53
+ max_stops=0, # nonstop only
54
+ sort=SORT_CHEAPEST, # cheapest first
55
+ airlines=["NH", "JL"], # filter to specific carriers
56
+ )
57
+ ```
58
+
59
+ ### Time window filtering
60
+
61
+ ```python
62
+ results = search(
63
+ "JFK", "LHR", "2026-06-15",
64
+ earliest_departure=8, # depart after 8am
65
+ latest_departure=14, # depart before 2pm
66
+ )
67
+ ```
68
+
69
+ ### Booking details (fare options)
70
+
71
+ ```python
72
+ from swoop import search, get_booking_results
73
+
74
+ results = search("JFK", "LAX", "2026-06-15")
75
+ itinerary = results.best[0]
76
+
77
+ # Get fare tiers — just pass the itinerary
78
+ options = get_booking_results(itinerary)
79
+
80
+ for opt in options:
81
+ print(f"${opt.price} — {opt.brand_label} ({opt.fare_family})")
82
+ ```
83
+
84
+ You can also pass a booking token string with explicit parameters:
85
+
86
+ ```python
87
+ options = get_booking_results(
88
+ itinerary.booking_token,
89
+ origin="JFK",
90
+ destination="LAX",
91
+ date="2026-06-15",
92
+ selected_legs=[
93
+ [
94
+ flight.departure_airport,
95
+ f"{flight.departure_date[0]}-{flight.departure_date[1]:02d}-{flight.departure_date[2]:02d}",
96
+ flight.arrival_airport,
97
+ None,
98
+ flight.airline,
99
+ flight.flight_number,
100
+ ]
101
+ for flight in itinerary.flights
102
+ ],
103
+ )
104
+ ```
105
+
106
+ ### Retry and timeout
107
+
108
+ ```python
109
+ # Retry up to 3 times on HTTP 429 (rate limit) with exponential backoff
110
+ results = search("JFK", "LAX", "2026-06-15", retries=3, timeout=90)
111
+
112
+ # Same for booking results
113
+ options = get_booking_results(itinerary, retries=2, timeout=60)
114
+ ```
115
+
116
+ ### Flight details
117
+
118
+ Each `Itinerary` contains detailed segment data:
119
+
120
+ ```python
121
+ results = search("JFK", "LAX", "2026-06-15")
122
+ for itinerary in results.best:
123
+ for flight in itinerary.flights:
124
+ print(f"{flight.airline} {flight.flight_number}")
125
+ print(f" {flight.departure_airport} → {flight.arrival_airport}")
126
+ print(f" Aircraft: {flight.aircraft}")
127
+ print(f" Legroom: {flight.legroom}")
128
+ if flight.co2_grams:
129
+ print(f" CO₂: {flight.co2_grams}g")
130
+
131
+ if itinerary.carbon_emissions:
132
+ ce = itinerary.carbon_emissions
133
+ print(f" Route emissions: {ce.difference_percent}% vs typical")
134
+
135
+ if results.price_range:
136
+ print(f" Price range: ${results.price_range.low}–${results.price_range.high}")
137
+ ```
138
+
139
+ ## Error handling
140
+
141
+ ```python
142
+ from swoop import search, SwoopHTTPError, SwoopRateLimitError, SwoopParseError
143
+
144
+ try:
145
+ results = search("JFK", "LAX", "2026-06-15")
146
+ except SwoopRateLimitError:
147
+ print("Rate limited — wait a few minutes")
148
+ except SwoopHTTPError as e:
149
+ print(f"HTTP {e.status_code}")
150
+ except SwoopParseError as e:
151
+ print(f"Parse error: {e}")
152
+ ```
153
+
154
+ ## API reference
155
+
156
+ ### `search(origin, destination, date, **kwargs)`
157
+
158
+ Search Google Flights and return a `SearchResult`.
159
+
160
+ | Parameter | Type | Default | Description |
161
+ |-----------|------|---------|-------------|
162
+ | `origin` | `str` | required | Origin IATA code |
163
+ | `destination` | `str` | required | Destination IATA code |
164
+ | `date` | `str` | required | Departure date (`YYYY-MM-DD`) |
165
+ | `return_date` | `str \| None` | `None` | Return date for roundtrip |
166
+ | `cabin` | `str` | `"economy"` | `economy`, `premium-economy`, `business`, `first` |
167
+ | `adults` | `int` | `1` | Number of adults |
168
+ | `max_stops` | `int \| None` | `None` | `None`=any, `0`=nonstop, `1`=1 stop, `2`=2 stops |
169
+ | `sort` | `int` | `SORT_DEPARTURE_TIME` | Sort order constant |
170
+ | `airlines` | `list[str] \| None` | `None` | Filter by airline codes |
171
+ | `include_basic_economy` | `bool` | `False` | Include basic economy fares |
172
+ | `timeout` | `int` | `90` | HTTP timeout in seconds |
173
+ | `retries` | `int` | `0` | Retries on HTTP 429 with exponential backoff |
174
+
175
+ Returns `SearchResult | None`. `None` means no results found.
176
+
177
+ ### `get_booking_results(itinerary_or_token, **kwargs)`
178
+
179
+ Get fare options for a specific itinerary. Pass an `Itinerary` object directly, or a booking token string with explicit `origin`, `destination`, `date`, and `selected_legs`. Returns `list[BookingOption]` with `price`, `brand_label`, `brand_code`, `fare_family`, etc. `BookingOption` supports both attribute access (`opt.price`) and dict-style access (`opt["price"]`, `opt.get("price")`).
180
+
181
+ ### Result types
182
+
183
+ - **`SearchResult`** — `best: list[Itinerary]`, `other: list[Itinerary]`, `price_range: PriceRange | None`
184
+ - **`Itinerary`** — Full itinerary with `price`, `flights`, `layovers`, `travel_time`, `booking_token`, `carbon_emissions`
185
+ - **`Flight`** — Segment details: `airline`, `flight_number`, `aircraft`, `legroom`, `co2_grams`, `amenities`
186
+ - **`Layover`** — Stop info: `minutes`, airports, `is_overnight`
187
+ - **`CarbonEmissions`** — `this_flight_grams`, `typical_for_route_grams`, `difference_percent`
188
+
189
+ ### Constants
190
+
191
+ | Constant | Value | Description |
192
+ |----------|-------|-------------|
193
+ | `SORT_TOP` | `1` | Google's default ranking |
194
+ | `SORT_CHEAPEST` | `2` | Cheapest first |
195
+ | `SORT_DEPARTURE_TIME` | `3` | By departure time |
196
+ | `SORT_ARRIVAL_TIME` | `4` | By arrival time |
197
+ | `SORT_DURATION` | `5` | Shortest first |
198
+
199
+ ## Pricing notes
200
+
201
+ Use `itinerary.price` to get the USD price as an integer.
202
+
203
+ By default, `search()` excludes basic economy fares so prices reflect Main Cabin. Pass `include_basic_economy=True` to include them:
204
+
205
+ ```python
206
+ results = search("JFK", "LAX", "2026-06-15", include_basic_economy=True)
207
+ ```
208
+
209
+ ## How it works
210
+
211
+ Swoop uses Google Flights' internal `GetShoppingResults` and `GetBookingResults` RPC endpoints — the same ones the web app calls when you search for flights. Requests are serialized as nested JSON payloads and sent via HTTP POST with browser impersonation (via [primp](https://github.com/deedy5/primp)).
212
+
213
+ Responses are decoded from nested list structures into typed Python dataclasses.
214
+
215
+ ## Dependencies
216
+
217
+ - **[primp](https://github.com/deedy5/primp)** — HTTP client with browser TLS impersonation
218
+ - **[protobuf](https://pypi.org/project/protobuf/)** — Protocol buffer serialization
219
+
220
+ ## License
221
+
222
+ MIT
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "swoop-flights"
7
+ dynamic = ["version"]
8
+ description = "Google Flights price scraper. Search flights programmatically via the same RPC endpoints the web app uses."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Ayush Saraswat" }]
13
+ keywords = ["google-flights", "flights", "travel", "scraper", "prices"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Internet :: WWW/HTTP",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "primp>=1.0.0",
28
+ "protobuf>=4.0.0",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ validation = ["airportsdata>=1.3.0"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/saraswatayu/swoop"
36
+ Issues = "https://github.com/saraswatayu/swoop/issues"
37
+
38
+ [tool.hatch.version]
39
+ path = "swoop/__init__.py"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["swoop"]
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ include = ["swoop/", "README.md", "LICENSE"]
46
+
47
+ [tool.pyright]
48
+ pythonVersion = "3.10"
49
+ typeCheckingMode = "basic"
50
+ exclude = ["swoop/flights_pb2.py", "tests/"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ markers = [
55
+ "live: integration tests that hit real Google Flights API (deselect with -m 'not live')",
56
+ ]
57
+
58
+ [tool.mutmut]
59
+ paths_to_mutate = "swoop/decoder.py,swoop/rpc.py,swoop/_validate.py,swoop/_booking.py"
60
+ tests_dir = "tests/"
61
+ runner = "python -m pytest tests/ -x --ignore=tests/test_booking_options_corpus.py --ignore=tests/test_booking_option_schema_hypotheses.py --ignore=tests/test_corpus_matrix_tools.py --ignore=tests/test_booking_option_field_explorer.py -m 'not live' -q"