rollinggo-flight 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,10 @@
1
+ rollinggo-npm-cli/node_modules/
2
+ rollinggo-npm-cli/dist/
3
+ rollinggo-npm-cli/*.tgz
4
+
5
+ rollinggo-uv-cli/.venv/
6
+ rollinggo-uv-cli/.pytest_cache/
7
+ rollinggo-uv-cli/__pycache__/
8
+ rollinggo-uv-cli/dist/
9
+ rollinggo-uv-cli/src/**/__pycache__/
10
+ rollinggo-uv-cli/tests/**/__pycache__/
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: rollinggo-flight
3
+ Version: 0.1.0
4
+ Summary: RollingGo flight search CLI package for uvx.
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27.0
7
+ Requires-Dist: pydantic>=2.8.0
8
+ Requires-Dist: rich>=13.7.0
9
+ Requires-Dist: typer>=0.12.3
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.3.2; extra == 'dev'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # RollingGo Flight CLI
15
+
16
+ RollingGo flight search CLI for uvx.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ uvx rollinggo-flight search-airports --keyword Hangzhou
22
+ ```
23
+
24
+ ## Commands
25
+
26
+ ### search-airports
27
+
28
+ Search airports by keyword.
29
+
30
+ ```bash
31
+ rollinggo-flight search-airports --api-key <key> --keyword "Hangzhou"
32
+ ```
33
+
34
+ ### search-flights
35
+
36
+ Search flights with structured filters.
37
+
38
+ ```bash
39
+ rollinggo-flight search-flights --api-key <key> \
40
+ --from-city HGH --to-city CTU \
41
+ --from-date 2026-05-01 --trip-type ONE_WAY \
42
+ --adult-number 1 --child-number 0 --cabin-grade ECONOMY
43
+ ```
@@ -0,0 +1,30 @@
1
+ # RollingGo Flight CLI
2
+
3
+ RollingGo flight search CLI for uvx.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ uvx rollinggo-flight search-airports --keyword Hangzhou
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### search-airports
14
+
15
+ Search airports by keyword.
16
+
17
+ ```bash
18
+ rollinggo-flight search-airports --api-key <key> --keyword "Hangzhou"
19
+ ```
20
+
21
+ ### search-flights
22
+
23
+ Search flights with structured filters.
24
+
25
+ ```bash
26
+ rollinggo-flight search-flights --api-key <key> \
27
+ --from-city HGH --to-city CTU \
28
+ --from-date 2026-05-01 --trip-type ONE_WAY \
29
+ --adult-number 1 --child-number 0 --cabin-grade ECONOMY
30
+ ```
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "rollinggo-flight"
3
+ version = "0.1.0"
4
+ description = "RollingGo flight search CLI package for uvx."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "httpx>=0.27.0",
9
+ "pydantic>=2.8.0",
10
+ "rich>=13.7.0",
11
+ "typer>=0.12.3",
12
+ ]
13
+
14
+ [project.optional-dependencies]
15
+ dev = [
16
+ "pytest>=8.3.2",
17
+ ]
18
+
19
+ [project.scripts]
20
+ rollinggo-flight = "rollinggo_cli.cli:main"
21
+
22
+ [build-system]
23
+ requires = ["hatchling>=1.25.0"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/rollinggo_cli"]
28
+
29
+ [tool.pytest.ini_options]
30
+ addopts = "-q"
31
+ testpaths = ["tests"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,67 @@
1
+ import os
2
+ from typing import Any
3
+
4
+ import httpx
5
+
6
+ from .constants import DEFAULT_BASE_URL
7
+ from .errors import ApiRequestError, CliValidationError
8
+
9
+
10
+ def normalize_base_url(base_url: str | None) -> str:
11
+ return (base_url or DEFAULT_BASE_URL).rstrip("/")
12
+
13
+
14
+ def resolve_api_key(cli_api_key: str | None) -> str:
15
+ if cli_api_key:
16
+ return cli_api_key
17
+
18
+ env_api_key = os.getenv("ROLLINGGO_API_KEY")
19
+ if env_api_key:
20
+ return env_api_key
21
+
22
+ raise CliValidationError("Missing API key. Pass --api-key or set ROLLINGGO_API_KEY.")
23
+
24
+
25
+ def request_api(
26
+ method: str,
27
+ endpoint: str,
28
+ api_key: str,
29
+ *,
30
+ base_url: str | None = None,
31
+ payload: dict[str, Any] | None = None,
32
+ transport: httpx.BaseTransport | None = None,
33
+ ) -> Any:
34
+ headers = {
35
+ "Authorization": f"Bearer {api_key}",
36
+ "Accept": "application/json",
37
+ }
38
+ if method.upper() == "POST":
39
+ headers["Content-Type"] = "application/json"
40
+
41
+ try:
42
+ with httpx.Client(
43
+ base_url=normalize_base_url(base_url),
44
+ timeout=30.0,
45
+ follow_redirects=True,
46
+ transport=transport,
47
+ ) as client:
48
+ response = client.request(method.upper(), endpoint, json=payload, headers=headers)
49
+ response.raise_for_status()
50
+ try:
51
+ return response.json()
52
+ except ValueError as exc:
53
+ raise ApiRequestError(
54
+ f"HTTP request succeeded but returned invalid JSON: {exc}"
55
+ ) from exc
56
+ except httpx.HTTPStatusError as exc:
57
+ body = ""
58
+ try:
59
+ body = exc.response.text
60
+ except Exception:
61
+ body = ""
62
+ message = body or str(exc)
63
+ raise ApiRequestError(
64
+ f"HTTP request failed with status {exc.response.status_code}: {message}"
65
+ ) from exc
66
+ except httpx.HTTPError as exc:
67
+ raise ApiRequestError(f"HTTP request failed: {exc}") from exc
@@ -0,0 +1,242 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from .api import request_api, resolve_api_key
8
+ from .constants import CABIN_GRADES, DEFAULT_BASE_URL, TRIP_TYPES
9
+ from .errors import ApiRequestError, CliValidationError
10
+ from .models import build_airport_search_payload, build_flight_search_payload
11
+ from .output import print_airport_table, print_flight_table, print_json
12
+
13
+ AI_HELP_TEXT = (
14
+ "RollingGo flight CLI.\n\n"
15
+ "Recommended for AI agents: call standard subcommands with structured options, "
16
+ "for example `rollinggo-flight search-airports --keyword ...`. "
17
+ "Results are written to stdout as JSON by default.\n\n"
18
+ "Parameter discovery: use `rollinggo-flight <command> --help` to inspect required options, "
19
+ "accepted value formats, and command examples."
20
+ )
21
+
22
+ SEARCH_AIRPORTS_EXAMPLE = (
23
+ "Minimal example:\n"
24
+ ' rollinggo-flight search-airports --api-key <key> --keyword "Hangzhou"'
25
+ )
26
+
27
+ SEARCH_FLIGHTS_EXAMPLE = (
28
+ "Minimal example:\n"
29
+ ' rollinggo-flight search-flights --api-key <key> --from-city HGH --to-city CTU '
30
+ '--from-date 2026-05-01 --trip-type ONE_WAY --adult-number 1 --child-number 0 --cabin-grade ECONOMY'
31
+ )
32
+
33
+ app = typer.Typer(
34
+ no_args_is_help=True,
35
+ add_completion=False,
36
+ help=AI_HELP_TEXT,
37
+ )
38
+
39
+
40
+ def _handle_error(exc: Exception, exit_code: int) -> None:
41
+ typer.echo(str(exc), err=True)
42
+ raise typer.Exit(code=exit_code) from exc
43
+
44
+
45
+ def _resolve_format(output_format: str, *, allow_table: bool) -> str:
46
+ if output_format not in ("json", "table"):
47
+ raise CliValidationError("Output format must be json or table.")
48
+ if output_format == "table" and not allow_table:
49
+ raise CliValidationError("--format table is only supported by search-airports and search-flights.")
50
+ return output_format
51
+
52
+
53
+ @app.command(
54
+ "search-airports",
55
+ help="Search airports by keyword.",
56
+ epilog=SEARCH_AIRPORTS_EXAMPLE,
57
+ )
58
+ def search_airports(
59
+ keyword: Annotated[
60
+ str,
61
+ typer.Option(
62
+ "--keyword",
63
+ help="Search keyword. Use English city name, airport name, or IATA code. Examples: Hangzhou, HGH.",
64
+ ),
65
+ ],
66
+ api_key: Annotated[
67
+ str | None,
68
+ typer.Option(
69
+ "--api-key",
70
+ help="RollingGo API key. If omitted, the CLI falls back to ROLLINGGO_API_KEY.",
71
+ ),
72
+ ] = None,
73
+ base_url: Annotated[
74
+ str,
75
+ typer.Option(
76
+ "--base-url",
77
+ help="Base URL for the RollingGo flight API. Override only for testing or private deployments.",
78
+ ),
79
+ ] = DEFAULT_BASE_URL,
80
+ output_format: Annotated[
81
+ str,
82
+ typer.Option(
83
+ "--format",
84
+ help="Output format. Use json for machine parsing.",
85
+ ),
86
+ ] = "json",
87
+ ) -> None:
88
+ try:
89
+ output_format = _resolve_format(output_format, allow_table=True)
90
+ payload = build_airport_search_payload(keyword=keyword)
91
+ result = request_api(
92
+ "POST",
93
+ "/api/mcp/airportsearch",
94
+ resolve_api_key(api_key),
95
+ base_url=base_url,
96
+ payload=payload,
97
+ )
98
+ if output_format == "table":
99
+ print_airport_table(result)
100
+ else:
101
+ print_json(result)
102
+ except CliValidationError as exc:
103
+ _handle_error(exc, 2)
104
+ except ApiRequestError as exc:
105
+ _handle_error(exc, 1)
106
+
107
+
108
+ @app.command(
109
+ "search-flights",
110
+ help="Search flights with structured filters.",
111
+ epilog=SEARCH_FLIGHTS_EXAMPLE,
112
+ )
113
+ def search_flights(
114
+ from_date: Annotated[
115
+ str,
116
+ typer.Option(
117
+ "--from-date",
118
+ help="Departure date in YYYY-MM-DD format.",
119
+ ),
120
+ ],
121
+ trip_type: Annotated[
122
+ str,
123
+ typer.Option(
124
+ "--trip-type",
125
+ help=f"Trip type. Supported values: {', '.join(TRIP_TYPES)}.",
126
+ ),
127
+ ],
128
+ adult_number: Annotated[
129
+ int,
130
+ typer.Option(
131
+ "--adult-number",
132
+ help="Number of adults. Integer >= 1.",
133
+ ),
134
+ ],
135
+ child_number: Annotated[
136
+ int,
137
+ typer.Option(
138
+ "--child-number",
139
+ help="Number of children. Integer >= 0.",
140
+ ),
141
+ ],
142
+ cabin_grade: Annotated[
143
+ str,
144
+ typer.Option(
145
+ "--cabin-grade",
146
+ help=f"Cabin class. Supported values: {', '.join(CABIN_GRADES)}.",
147
+ ),
148
+ ],
149
+ api_key: Annotated[
150
+ str | None,
151
+ typer.Option(
152
+ "--api-key",
153
+ help="RollingGo API key. If omitted, the CLI falls back to ROLLINGGO_API_KEY.",
154
+ ),
155
+ ] = None,
156
+ base_url: Annotated[
157
+ str,
158
+ typer.Option(
159
+ "--base-url",
160
+ help="Base URL for the RollingGo flight API. Override only for testing or private deployments.",
161
+ ),
162
+ ] = DEFAULT_BASE_URL,
163
+ output_format: Annotated[
164
+ str,
165
+ typer.Option(
166
+ "--format",
167
+ help="Output format. Use json for machine parsing.",
168
+ ),
169
+ ] = "json",
170
+ ret_date: Annotated[
171
+ str | None,
172
+ typer.Option(
173
+ "--ret-date",
174
+ help="Return date in YYYY-MM-DD format. Required when --trip-type is ROUND_TRIP.",
175
+ ),
176
+ ] = None,
177
+ from_city: Annotated[
178
+ str | None,
179
+ typer.Option(
180
+ "--from-city",
181
+ help="Departure city code. Mutually exclusive with --from-airport.",
182
+ ),
183
+ ] = None,
184
+ from_airport: Annotated[
185
+ str | None,
186
+ typer.Option(
187
+ "--from-airport",
188
+ help="Departure airport code (IATA). Mutually exclusive with --from-city.",
189
+ ),
190
+ ] = None,
191
+ to_city: Annotated[
192
+ str | None,
193
+ typer.Option(
194
+ "--to-city",
195
+ help="Arrival city code. Mutually exclusive with --to-airport.",
196
+ ),
197
+ ] = None,
198
+ to_airport: Annotated[
199
+ str | None,
200
+ typer.Option(
201
+ "--to-airport",
202
+ help="Arrival airport code (IATA). Mutually exclusive with --to-city.",
203
+ ),
204
+ ] = None,
205
+ ) -> None:
206
+ try:
207
+ output_format = _resolve_format(output_format, allow_table=True)
208
+ payload = build_flight_search_payload(
209
+ adult_number=adult_number,
210
+ child_number=child_number,
211
+ cabin_grade=cabin_grade,
212
+ from_date=from_date,
213
+ trip_type=trip_type,
214
+ ret_date=ret_date,
215
+ from_city=from_city,
216
+ from_airport=from_airport,
217
+ to_city=to_city,
218
+ to_airport=to_airport,
219
+ )
220
+ result = request_api(
221
+ "POST",
222
+ "/api/mcp/flightsearch",
223
+ resolve_api_key(api_key),
224
+ base_url=base_url,
225
+ payload=payload,
226
+ )
227
+ if output_format == "table":
228
+ print_flight_table(result)
229
+ else:
230
+ print_json(result)
231
+ except CliValidationError as exc:
232
+ _handle_error(exc, 2)
233
+ except ApiRequestError as exc:
234
+ _handle_error(exc, 1)
235
+
236
+
237
+ def main() -> None:
238
+ app()
239
+
240
+
241
+ if __name__ == "__main__":
242
+ main()
@@ -0,0 +1,36 @@
1
+ import os
2
+
3
+ BASE_URL_STAGING = "https://travelportal-api-staging.aigohotel.com"
4
+ BASE_URL_PRODUCTION = "https://mcp.rollinggo.cn"
5
+ DEFAULT_BASE_URL = os.getenv("ROLLINGGO_API_BASE_URL", BASE_URL_STAGING)
6
+
7
+ CABIN_GRADES = (
8
+ "ECONOMY",
9
+ "PREMIUM_ECONOMY",
10
+ "BUSINESS",
11
+ "FIRST",
12
+ )
13
+
14
+ TRIP_TYPES = (
15
+ "ONE_WAY",
16
+ "ROUND_TRIP",
17
+ )
18
+
19
+ AIRPORT_TABLE_COLUMNS = [
20
+ ("airportCode", "airportCode"),
21
+ ("airportName", "airportName"),
22
+ ("cityCode", "cityCode"),
23
+ ("cityName", "cityName"),
24
+ ("countryCode", "countryCode"),
25
+ ("countryName", "countryName"),
26
+ ]
27
+
28
+ FLIGHT_TABLE_COLUMNS = [
29
+ ("flightNo", "flightNo"),
30
+ ("airlineName", "airlineName"),
31
+ ("fromAirport", "fromAirport"),
32
+ ("toAirport", "toAirport"),
33
+ ("fromDate", "fromDate"),
34
+ ("price", "price"),
35
+ ("currency", "currency"),
36
+ ]
@@ -0,0 +1,6 @@
1
+ class CliValidationError(Exception):
2
+ pass
3
+
4
+
5
+ class ApiRequestError(Exception):
6
+ pass
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from datetime import date
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator
8
+
9
+ from .constants import CABIN_GRADES, TRIP_TYPES
10
+ from .errors import CliValidationError
11
+
12
+ OutputFormat = Literal["json", "table"]
13
+
14
+ DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
15
+
16
+
17
+ def _error_message(exc: ValidationError) -> str:
18
+ first_error = exc.errors()[0]
19
+ location = ".".join(str(part) for part in first_error["loc"])
20
+ return f"Invalid value for {location}: {first_error['msg']}"
21
+
22
+
23
+ def _validate_date(value: str, field_name: str) -> None:
24
+ if not DATE_RE.fullmatch(value):
25
+ raise CliValidationError(f"{field_name} must use YYYY-MM-DD format.")
26
+
27
+
28
+ class AirportSearchInput(BaseModel):
29
+ keyword: str = Field(min_length=1)
30
+
31
+ def to_payload(self) -> dict[str, Any]:
32
+ return {"keyword": self.keyword}
33
+
34
+
35
+ class FlightSearchInput(BaseModel):
36
+ adult_number: int = Field(ge=1)
37
+ child_number: int = Field(ge=0)
38
+ cabin_grade: Literal["ECONOMY", "PREMIUM_ECONOMY", "BUSINESS", "FIRST"]
39
+ from_date: str
40
+ trip_type: Literal["ONE_WAY", "ROUND_TRIP"]
41
+ ret_date: str | None = None
42
+ from_city: str | None = None
43
+ from_airport: str | None = None
44
+ to_city: str | None = None
45
+ to_airport: str | None = None
46
+
47
+ @field_validator("from_date", "ret_date", mode="before")
48
+ @classmethod
49
+ def validate_date_format(cls, value: str | None) -> str | None:
50
+ if value is not None:
51
+ _validate_date(value, "date")
52
+ return value
53
+
54
+ @model_validator(mode="after")
55
+ def validate_flight_search(self) -> "FlightSearchInput":
56
+ if self.trip_type == "ROUND_TRIP" and not self.ret_date:
57
+ raise ValueError("retDate is required when tripType is ROUND_TRIP.")
58
+ if not self.from_city and not self.from_airport:
59
+ raise ValueError("Provide either from_city or from_airport.")
60
+ if not self.to_city and not self.to_airport:
61
+ raise ValueError("Provide either to_city or to_airport.")
62
+ return self
63
+
64
+ def to_payload(self) -> dict[str, Any]:
65
+ payload: dict[str, Any] = {
66
+ "adultNumber": self.adult_number,
67
+ "childNumber": self.child_number,
68
+ "cabinGrade": self.cabin_grade,
69
+ "fromDate": self.from_date,
70
+ "tripType": self.trip_type,
71
+ }
72
+
73
+ if self.ret_date:
74
+ payload["retDate"] = self.ret_date
75
+ if self.from_city:
76
+ payload["fromCity"] = self.from_city
77
+ if self.from_airport:
78
+ payload["fromAirport"] = self.from_airport
79
+ if self.to_city:
80
+ payload["toCity"] = self.to_city
81
+ if self.to_airport:
82
+ payload["toAirport"] = self.to_airport
83
+
84
+ return payload
85
+
86
+
87
+ def build_airport_search_payload(**kwargs: Any) -> dict[str, Any]:
88
+ try:
89
+ payload = AirportSearchInput(**kwargs)
90
+ except ValidationError as exc:
91
+ raise CliValidationError(_error_message(exc)) from exc
92
+ return payload.to_payload()
93
+
94
+
95
+ def build_flight_search_payload(**kwargs: Any) -> dict[str, Any]:
96
+ try:
97
+ payload = FlightSearchInput(**kwargs)
98
+ except ValidationError as exc:
99
+ raise CliValidationError(_error_message(exc)) from exc
100
+ return payload.to_payload()
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from typing import Any
6
+
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from .constants import AIRPORT_TABLE_COLUMNS, FLIGHT_TABLE_COLUMNS
11
+
12
+
13
+ def remove_field(data: Any, field_name: str) -> Any:
14
+ if isinstance(data, dict):
15
+ return {
16
+ key: remove_field(value, field_name)
17
+ for key, value in data.items()
18
+ if key != field_name
19
+ }
20
+ if isinstance(data, list):
21
+ return [remove_field(item, field_name) for item in data]
22
+ return data
23
+
24
+
25
+ def print_json(data: Any) -> None:
26
+ sys.stdout.write(json.dumps(data, ensure_ascii=False, indent=2))
27
+ sys.stdout.write("\n")
28
+
29
+
30
+ def _find_rows(data: Any, identifier_keys: tuple[str, ...]) -> list[dict[str, Any]]:
31
+ if isinstance(data, list):
32
+ return [row for row in data if isinstance(row, dict)]
33
+
34
+ if isinstance(data, dict):
35
+ if any(key in data for key in identifier_keys):
36
+ return [data]
37
+
38
+ for key in ("data", "airPortInformationList", "flightInformationList", "items", "results", "list"):
39
+ value = data.get(key)
40
+ if isinstance(value, list):
41
+ return [row for row in value if isinstance(row, dict)]
42
+ if isinstance(value, dict):
43
+ nested_rows = _find_rows(value, identifier_keys)
44
+ if nested_rows:
45
+ return nested_rows
46
+
47
+ for value in data.values():
48
+ nested_rows = _find_rows(value, identifier_keys)
49
+ if nested_rows:
50
+ return nested_rows
51
+
52
+ return []
53
+
54
+
55
+ def print_airport_table(data: Any) -> None:
56
+ table = Table()
57
+ for header, _ in AIRPORT_TABLE_COLUMNS:
58
+ table.add_column(header)
59
+
60
+ for row in _find_rows(data, ("airportCode", "airportName")):
61
+ table.add_row(*[str(row.get(key, "")) for _, key in AIRPORT_TABLE_COLUMNS])
62
+
63
+ console = Console(file=sys.stdout)
64
+ console.print(table)
65
+
66
+
67
+ def print_flight_table(data: Any) -> None:
68
+ table = Table()
69
+ for header, _ in FLIGHT_TABLE_COLUMNS:
70
+ table.add_column(header)
71
+
72
+ for row in _find_rows(data, ("flightNo", "airlineName")):
73
+ table.add_row(*[str(row.get(key, "")) for _, key in FLIGHT_TABLE_COLUMNS])
74
+
75
+ console = Console(file=sys.stdout)
76
+ console.print(table)
@@ -0,0 +1,102 @@
1
+ import pytest
2
+
3
+ from rollinggo_cli.models import (
4
+ build_airport_search_payload,
5
+ build_flight_search_payload,
6
+ )
7
+
8
+
9
+ def test_airport_search_payload():
10
+ payload = build_airport_search_payload(keyword="Hangzhou")
11
+ assert payload == {"keyword": "Hangzhou"}
12
+
13
+
14
+ def test_airport_search_payload_empty_keyword():
15
+ with pytest.raises(Exception):
16
+ build_airport_search_payload(keyword="")
17
+
18
+
19
+ def test_flight_search_one_way():
20
+ payload = build_flight_search_payload(
21
+ adult_number=1,
22
+ child_number=0,
23
+ cabin_grade="ECONOMY",
24
+ from_date="2026-05-01",
25
+ trip_type="ONE_WAY",
26
+ from_city="HGH",
27
+ to_city="CTU",
28
+ )
29
+ assert payload["adultNumber"] == 1
30
+ assert payload["childNumber"] == 0
31
+ assert payload["cabinGrade"] == "ECONOMY"
32
+ assert payload["fromDate"] == "2026-05-01"
33
+ assert payload["tripType"] == "ONE_WAY"
34
+ assert payload["fromCity"] == "HGH"
35
+ assert payload["toCity"] == "CTU"
36
+ assert "retDate" not in payload
37
+
38
+
39
+ def test_flight_search_round_trip():
40
+ payload = build_flight_search_payload(
41
+ adult_number=2,
42
+ child_number=1,
43
+ cabin_grade="BUSINESS",
44
+ from_date="2026-05-01",
45
+ trip_type="ROUND_TRIP",
46
+ ret_date="2026-05-10",
47
+ from_airport="HGH",
48
+ to_airport="TFU",
49
+ )
50
+ assert payload["retDate"] == "2026-05-10"
51
+ assert payload["fromAirport"] == "HGH"
52
+ assert payload["toAirport"] == "TFU"
53
+
54
+
55
+ def test_flight_search_round_trip_missing_ret_date():
56
+ with pytest.raises(Exception):
57
+ build_flight_search_payload(
58
+ adult_number=1,
59
+ child_number=0,
60
+ cabin_grade="ECONOMY",
61
+ from_date="2026-05-01",
62
+ trip_type="ROUND_TRIP",
63
+ from_city="HGH",
64
+ to_city="CTU",
65
+ )
66
+
67
+
68
+ def test_flight_search_missing_origin():
69
+ with pytest.raises(Exception):
70
+ build_flight_search_payload(
71
+ adult_number=1,
72
+ child_number=0,
73
+ cabin_grade="ECONOMY",
74
+ from_date="2026-05-01",
75
+ trip_type="ONE_WAY",
76
+ to_city="CTU",
77
+ )
78
+
79
+
80
+ def test_flight_search_missing_destination():
81
+ with pytest.raises(Exception):
82
+ build_flight_search_payload(
83
+ adult_number=1,
84
+ child_number=0,
85
+ cabin_grade="ECONOMY",
86
+ from_date="2026-05-01",
87
+ trip_type="ONE_WAY",
88
+ from_city="HGH",
89
+ )
90
+
91
+
92
+ def test_flight_search_invalid_date():
93
+ with pytest.raises(Exception):
94
+ build_flight_search_payload(
95
+ adult_number=1,
96
+ child_number=0,
97
+ cabin_grade="ECONOMY",
98
+ from_date="2026/05/01",
99
+ trip_type="ONE_WAY",
100
+ from_city="HGH",
101
+ to_city="CTU",
102
+ )