cli-web-tripadvisor 0.1.1__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.
- cli_web_tripadvisor-0.1.1/PKG-INFO +13 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/README.md +110 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/__init__.py +0 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/__main__.py +6 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/commands/__init__.py +0 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/commands/attractions.py +153 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/commands/hotels.py +155 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/commands/locations.py +93 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/commands/restaurants.py +157 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/core/__init__.py +0 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/core/client.py +732 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/core/exceptions.py +82 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/core/models.py +141 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/skills/SKILL.md +143 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/tests/TEST.md +212 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/tests/__init__.py +0 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/tests/test_core.py +813 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/tests/test_e2e.py +292 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/tripadvisor_cli.py +159 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/utils/__init__.py +0 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/utils/config.py +21 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/utils/doctor.py +188 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/utils/helpers.py +124 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/utils/mcp_server.py +290 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/utils/output.py +17 -0
- cli_web_tripadvisor-0.1.1/cli_web/tripadvisor/utils/repl_skin.py +486 -0
- cli_web_tripadvisor-0.1.1/cli_web_tripadvisor.egg-info/PKG-INFO +13 -0
- cli_web_tripadvisor-0.1.1/cli_web_tripadvisor.egg-info/SOURCES.txt +32 -0
- cli_web_tripadvisor-0.1.1/cli_web_tripadvisor.egg-info/dependency_links.txt +1 -0
- cli_web_tripadvisor-0.1.1/cli_web_tripadvisor.egg-info/entry_points.txt +2 -0
- cli_web_tripadvisor-0.1.1/cli_web_tripadvisor.egg-info/requires.txt +5 -0
- cli_web_tripadvisor-0.1.1/cli_web_tripadvisor.egg-info/top_level.txt +1 -0
- cli_web_tripadvisor-0.1.1/setup.cfg +4 -0
- cli_web_tripadvisor-0.1.1/setup.py +24 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cli-web-tripadvisor
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: CLI for TripAdvisor — search hotels, restaurants, and attractions
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: click>=8.0
|
|
7
|
+
Requires-Dist: curl_cffi>=0.5.10
|
|
8
|
+
Requires-Dist: beautifulsoup4>=4.12.0
|
|
9
|
+
Requires-Dist: rich>=13.0
|
|
10
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
11
|
+
Dynamic: requires-dist
|
|
12
|
+
Dynamic: requires-python
|
|
13
|
+
Dynamic: summary
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# cli-web-tripadvisor
|
|
2
|
+
|
|
3
|
+
> Generated by [CLI-Anything-Web](../../../../cli-anything-web-plugin/) from [tripadvisor.com](https://www.tripadvisor.com)
|
|
4
|
+
|
|
5
|
+
Agent-native CLI for TripAdvisor — search destinations, hotels, restaurants,
|
|
6
|
+
and attractions from the terminal. Data is extracted from SSR HTML pages
|
|
7
|
+
(JSON-LD structured data) and the TypeAheadJson REST endpoint; DataDome bot
|
|
8
|
+
protection is bypassed with curl_cffi Safari-iOS impersonation.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd tripadvisor/agent-harness
|
|
14
|
+
pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Binary: `cli-web-tripadvisor`
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 1. Resolve a destination to its geo_id
|
|
23
|
+
cli-web-tripadvisor locations search "Paris"
|
|
24
|
+
|
|
25
|
+
# 2. Search hotels (faster with a known geo_id)
|
|
26
|
+
cli-web-tripadvisor hotels search "Paris" --geo-id 187147
|
|
27
|
+
cli-web-tripadvisor hotels search "Paris" --page 2
|
|
28
|
+
|
|
29
|
+
# 3. Hotel detail by TripAdvisor URL
|
|
30
|
+
cli-web-tripadvisor hotels get "https://www.tripadvisor.com/Hotel_Review-g187147-d229968-Reviews-..."
|
|
31
|
+
|
|
32
|
+
# Restaurants
|
|
33
|
+
cli-web-tripadvisor restaurants search "New York City"
|
|
34
|
+
cli-web-tripadvisor restaurants get "https://www.tripadvisor.com/Restaurant_Review-..."
|
|
35
|
+
|
|
36
|
+
# Attractions / things to do
|
|
37
|
+
cli-web-tripadvisor attractions search "London" --page 2
|
|
38
|
+
cli-web-tripadvisor attractions get "https://www.tripadvisor.com/Attraction_Review-..."
|
|
39
|
+
|
|
40
|
+
# Interactive REPL (default when no subcommand is given)
|
|
41
|
+
cli-web-tripadvisor
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Commands
|
|
45
|
+
|
|
46
|
+
| Group | Command | Purpose |
|
|
47
|
+
|-------|---------|---------|
|
|
48
|
+
| `locations` | `search QUERY [--max N]` | Search destinations; returns `geo_id` for use with `--geo-id` (default max: 6) |
|
|
49
|
+
| `hotels` | `search LOCATION [--geo-id ID] [--page N]` | Search hotels in a location (30 per page) |
|
|
50
|
+
| `hotels` | `get URL` | Detailed hotel info from a TripAdvisor URL |
|
|
51
|
+
| `restaurants` | `search LOCATION [--geo-id ID] [--page N]` | Search restaurants in a location (30 per page) |
|
|
52
|
+
| `restaurants` | `get URL` | Detailed restaurant info from a TripAdvisor URL |
|
|
53
|
+
| `attractions` | `search LOCATION [--geo-id ID] [--page N]` | Search attractions/things to do (30 per page) |
|
|
54
|
+
| `attractions` | `get URL` | Detailed attraction info from a TripAdvisor URL |
|
|
55
|
+
| — | `doctor` | Environment / connectivity self-check |
|
|
56
|
+
| — | `mcp` | Run as an MCP server over stdio (every command exposed as a tool) |
|
|
57
|
+
|
|
58
|
+
Typical workflow: `locations search` → grab the `geo_id` → pass it to
|
|
59
|
+
`hotels`/`restaurants`/`attractions search` via `--geo-id` to skip the
|
|
60
|
+
lookup round-trip.
|
|
61
|
+
|
|
62
|
+
## Auth
|
|
63
|
+
|
|
64
|
+
**No authentication required.** All search, listing, and detail operations
|
|
65
|
+
use public TripAdvisor pages. There is no `login` command and no `auth.json`.
|
|
66
|
+
|
|
67
|
+
If requests start failing with `AUTH_EXPIRED` (HTTP 401/403), DataDome bot
|
|
68
|
+
protection has triggered — the impersonation profile can be overridden via:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
export CLI_WEB_TRIPADVISOR_IMPERSONATE=safari17_2_ios # default
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## JSON Output
|
|
75
|
+
|
|
76
|
+
Every command supports `--json` (global or per-command) for structured,
|
|
77
|
+
agent-friendly output:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cli-web-tripadvisor --json locations search "Paris"
|
|
81
|
+
cli-web-tripadvisor hotels search "Paris" --geo-id 187147 --json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Errors are also structured in JSON mode:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{"error": true, "code": "NOT_FOUND", "message": "Page not found: ..."}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## REPL
|
|
91
|
+
|
|
92
|
+
Running `cli-web-tripadvisor` with no subcommand opens an interactive REPL.
|
|
93
|
+
Type `help` for the command list, `quit`/`exit` to leave. Quoted arguments
|
|
94
|
+
work (`hotels search "New York City"`), and starting the REPL with `--json`
|
|
95
|
+
makes every REPL command emit JSON.
|
|
96
|
+
|
|
97
|
+
## Testing
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
cd tripadvisor/agent-harness
|
|
101
|
+
pip install -e .
|
|
102
|
+
|
|
103
|
+
# Unit tests (offline — HTTP layer is mocked)
|
|
104
|
+
python -m pytest cli_web/tripadvisor/tests/test_core.py -v -s
|
|
105
|
+
|
|
106
|
+
# E2E tests (live site + subprocess CLI tests)
|
|
107
|
+
python -m pytest cli_web/tripadvisor/tests/test_e2e.py -v -s
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
See `TRIPADVISOR.md` (in `agent-harness/`) for the full API map.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Attraction commands for cli-web-tripadvisor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..core.client import TripAdvisorClient
|
|
10
|
+
from ..utils.helpers import (
|
|
11
|
+
format_rating,
|
|
12
|
+
handle_errors,
|
|
13
|
+
print_json,
|
|
14
|
+
resolve_json_mode,
|
|
15
|
+
truncate,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group("attractions")
|
|
22
|
+
@click.pass_context
|
|
23
|
+
def attractions(ctx):
|
|
24
|
+
"""Search and browse TripAdvisor attractions and things to do."""
|
|
25
|
+
ctx.ensure_object(dict)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@attractions.command("search")
|
|
29
|
+
@click.argument("location")
|
|
30
|
+
@click.option(
|
|
31
|
+
"--geo-id", default=None, metavar="ID", help="Use known geo_id to skip location lookup."
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--page", default=1, type=int, show_default=True, help="Page number (30 attractions per page)."
|
|
35
|
+
)
|
|
36
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
37
|
+
@click.pass_context
|
|
38
|
+
def search_attractions(ctx, location, geo_id, page, json_mode):
|
|
39
|
+
"""Search attractions and things to do in LOCATION.
|
|
40
|
+
|
|
41
|
+
LOCATION is a destination name like "Paris" or "New York City".
|
|
42
|
+
Use --geo-id to skip the location-lookup step (faster).
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
|
|
46
|
+
cli-web-tripadvisor attractions search "Paris"
|
|
47
|
+
|
|
48
|
+
cli-web-tripadvisor attractions search "Paris" --geo-id 187147
|
|
49
|
+
|
|
50
|
+
cli-web-tripadvisor attractions search "London" --page 2
|
|
51
|
+
|
|
52
|
+
cli-web-tripadvisor attractions search "Tokyo" --json
|
|
53
|
+
"""
|
|
54
|
+
json_mode = resolve_json_mode(json_mode, ctx)
|
|
55
|
+
|
|
56
|
+
with handle_errors(json_mode=json_mode):
|
|
57
|
+
with TripAdvisorClient() as client:
|
|
58
|
+
result = client.search_attractions(location, geo_id=geo_id, page=page)
|
|
59
|
+
|
|
60
|
+
attrlist = result.get("attractions", [])
|
|
61
|
+
|
|
62
|
+
if json_mode:
|
|
63
|
+
print_json(
|
|
64
|
+
{
|
|
65
|
+
"success": True,
|
|
66
|
+
"data": {
|
|
67
|
+
"location": location,
|
|
68
|
+
"geo_id": result.get("geo_id"),
|
|
69
|
+
"page": page,
|
|
70
|
+
"count": len(attrlist),
|
|
71
|
+
"attractions": [a.to_dict() for a in attrlist],
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
if not attrlist:
|
|
78
|
+
click.echo(
|
|
79
|
+
f"No attractions found for '{location}' (page {page}). "
|
|
80
|
+
"Try a different location name or --geo-id."
|
|
81
|
+
)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
table = Table(
|
|
85
|
+
title=f"Attractions in {location} — page {page}",
|
|
86
|
+
show_lines=False,
|
|
87
|
+
expand=False,
|
|
88
|
+
)
|
|
89
|
+
table.add_column("ID", style="dim", no_wrap=True, max_width=10)
|
|
90
|
+
table.add_column("Name", max_width=40)
|
|
91
|
+
table.add_column("Rating", justify="right", max_width=14)
|
|
92
|
+
table.add_column("City", max_width=16)
|
|
93
|
+
table.add_column("Phone", max_width=18)
|
|
94
|
+
|
|
95
|
+
for a in attrlist:
|
|
96
|
+
table.add_row(
|
|
97
|
+
a.id,
|
|
98
|
+
truncate(a.name, 40),
|
|
99
|
+
format_rating(a.rating, a.review_count),
|
|
100
|
+
a.city or "—",
|
|
101
|
+
a.telephone or "—",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
console.print(table)
|
|
105
|
+
click.echo(f"\nShowing {len(attrlist)} attraction(s) on page {page}.")
|
|
106
|
+
if len(attrlist) >= 30:
|
|
107
|
+
click.echo(f"Next page: attractions search '{location}' --page {page + 1}")
|
|
108
|
+
click.echo("Tip: Use 'attractions get URL' with the attraction URL to see full details.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@attractions.command("get")
|
|
112
|
+
@click.argument("url")
|
|
113
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
114
|
+
@click.pass_context
|
|
115
|
+
def get_attraction(ctx, url, json_mode):
|
|
116
|
+
"""Get detailed information for an attraction by its TripAdvisor URL.
|
|
117
|
+
|
|
118
|
+
The URL is the full TripAdvisor attraction URL from a search result,
|
|
119
|
+
e.g. https://www.tripadvisor.com/Attraction_Review-g187147-d188151-Reviews-...
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
|
|
123
|
+
cli-web-tripadvisor attractions get "https://www.tripadvisor.com/Attraction_Review-g187147-d188151-Reviews-Eiffel_Tower-Paris_Ile_de_France.html"
|
|
124
|
+
|
|
125
|
+
cli-web-tripadvisor attractions get "https://www.tripadvisor.com/Attraction_Review-..." --json
|
|
126
|
+
"""
|
|
127
|
+
json_mode = resolve_json_mode(json_mode, ctx)
|
|
128
|
+
|
|
129
|
+
with handle_errors(json_mode=json_mode):
|
|
130
|
+
with TripAdvisorClient() as client:
|
|
131
|
+
attr = client.get_attraction(url)
|
|
132
|
+
|
|
133
|
+
if json_mode:
|
|
134
|
+
print_json({"success": True, "data": {"attraction": attr.to_dict()}})
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
click.echo(f"\n{'=' * 60}")
|
|
138
|
+
click.echo(f" {attr.name}")
|
|
139
|
+
click.echo(f"{'=' * 60}")
|
|
140
|
+
click.echo(f" ID: {attr.id or '—'}")
|
|
141
|
+
click.echo(f" Rating: {format_rating(attr.rating, attr.review_count)}")
|
|
142
|
+
click.echo(f" Address: {attr.address or '—'}")
|
|
143
|
+
click.echo(f" City: {attr.city or '—'}")
|
|
144
|
+
click.echo(f" Telephone: {attr.telephone or '—'}")
|
|
145
|
+
click.echo(f" Coordinates: {attr.latitude or '?'}, {attr.longitude or '?'}")
|
|
146
|
+
if attr.opening_hours:
|
|
147
|
+
click.echo(" Hours:")
|
|
148
|
+
for h in attr.opening_hours[:7]:
|
|
149
|
+
click.echo(f" {h}")
|
|
150
|
+
if attr.description:
|
|
151
|
+
click.echo(f" Description: {truncate(attr.description, 200)}")
|
|
152
|
+
click.echo(f" URL: {attr.url}")
|
|
153
|
+
click.echo()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Hotel commands for cli-web-tripadvisor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..core.client import TripAdvisorClient
|
|
10
|
+
from ..utils.helpers import (
|
|
11
|
+
format_rating,
|
|
12
|
+
handle_errors,
|
|
13
|
+
print_json,
|
|
14
|
+
resolve_json_mode,
|
|
15
|
+
truncate,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group("hotels")
|
|
22
|
+
@click.pass_context
|
|
23
|
+
def hotels(ctx):
|
|
24
|
+
"""Search and browse TripAdvisor hotels."""
|
|
25
|
+
ctx.ensure_object(dict)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@hotels.command("search")
|
|
29
|
+
@click.argument("location")
|
|
30
|
+
@click.option(
|
|
31
|
+
"--geo-id", default=None, metavar="ID", help="Use known geo_id to skip location lookup."
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--page", default=1, type=int, show_default=True, help="Page number (30 hotels per page)."
|
|
35
|
+
)
|
|
36
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
37
|
+
@click.pass_context
|
|
38
|
+
def search_hotels(ctx, location, geo_id, page, json_mode):
|
|
39
|
+
"""Search hotels in LOCATION.
|
|
40
|
+
|
|
41
|
+
LOCATION is a destination name like "Paris" or "New York City".
|
|
42
|
+
Use --geo-id to skip the location-lookup step (faster).
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
|
|
46
|
+
cli-web-tripadvisor hotels search "Paris"
|
|
47
|
+
|
|
48
|
+
cli-web-tripadvisor hotels search "Paris" --geo-id 187147
|
|
49
|
+
|
|
50
|
+
cli-web-tripadvisor hotels search "Tokyo" --page 2
|
|
51
|
+
|
|
52
|
+
cli-web-tripadvisor hotels search "London" --json
|
|
53
|
+
"""
|
|
54
|
+
json_mode = resolve_json_mode(json_mode, ctx)
|
|
55
|
+
|
|
56
|
+
with handle_errors(json_mode=json_mode):
|
|
57
|
+
with TripAdvisorClient() as client:
|
|
58
|
+
result = client.search_hotels(location, geo_id=geo_id, page=page)
|
|
59
|
+
|
|
60
|
+
hotellist = result.get("hotels", [])
|
|
61
|
+
|
|
62
|
+
if json_mode:
|
|
63
|
+
print_json(
|
|
64
|
+
{
|
|
65
|
+
"success": True,
|
|
66
|
+
"data": {
|
|
67
|
+
"location": location,
|
|
68
|
+
"geo_id": result.get("geo_id"),
|
|
69
|
+
"page": page,
|
|
70
|
+
"count": len(hotellist),
|
|
71
|
+
"hotels": [h.to_dict() for h in hotellist],
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
if not hotellist:
|
|
78
|
+
click.echo(
|
|
79
|
+
f"No hotels found for '{location}' (page {page}). "
|
|
80
|
+
"Try a different location name or --geo-id."
|
|
81
|
+
)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
table = Table(
|
|
85
|
+
title=f"Hotels in {location} — page {page}",
|
|
86
|
+
show_lines=False,
|
|
87
|
+
expand=False,
|
|
88
|
+
)
|
|
89
|
+
table.add_column("ID", style="dim", no_wrap=True, max_width=10)
|
|
90
|
+
table.add_column("Name", max_width=38)
|
|
91
|
+
table.add_column("Rating", justify="right", max_width=14)
|
|
92
|
+
table.add_column("Price", justify="center", max_width=8)
|
|
93
|
+
table.add_column("City", max_width=16)
|
|
94
|
+
table.add_column("Phone", max_width=18)
|
|
95
|
+
|
|
96
|
+
for h in hotellist:
|
|
97
|
+
table.add_row(
|
|
98
|
+
h.id,
|
|
99
|
+
truncate(h.name, 38),
|
|
100
|
+
format_rating(h.rating, h.review_count),
|
|
101
|
+
h.price_range or "—",
|
|
102
|
+
h.city or "—",
|
|
103
|
+
h.telephone or "—",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
console.print(table)
|
|
107
|
+
click.echo(f"\nShowing {len(hotellist)} hotel(s) on page {page}.")
|
|
108
|
+
if len(hotellist) >= 30:
|
|
109
|
+
click.echo(f"Next page: hotels search '{location}' --page {page + 1}")
|
|
110
|
+
click.echo("Tip: Use 'hotels get URL' with the hotel URL to see full details.")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@hotels.command("get")
|
|
114
|
+
@click.argument("url")
|
|
115
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
116
|
+
@click.pass_context
|
|
117
|
+
def get_hotel(ctx, url, json_mode):
|
|
118
|
+
"""Get detailed information for a hotel by its TripAdvisor URL.
|
|
119
|
+
|
|
120
|
+
The URL is the full TripAdvisor hotel URL from a search result,
|
|
121
|
+
e.g. https://www.tripadvisor.com/Hotel_Review-g187147-d229968-Reviews-...
|
|
122
|
+
|
|
123
|
+
Examples:
|
|
124
|
+
|
|
125
|
+
cli-web-tripadvisor hotels get "https://www.tripadvisor.com/Hotel_Review-g187147-d229968-Reviews-Hotel_Astra_Opera_Astotel-Paris_Ile_de_France.html"
|
|
126
|
+
|
|
127
|
+
cli-web-tripadvisor hotels get "https://www.tripadvisor.com/Hotel_Review-..." --json
|
|
128
|
+
"""
|
|
129
|
+
json_mode = resolve_json_mode(json_mode, ctx)
|
|
130
|
+
|
|
131
|
+
with handle_errors(json_mode=json_mode):
|
|
132
|
+
with TripAdvisorClient() as client:
|
|
133
|
+
hotel = client.get_hotel(url)
|
|
134
|
+
|
|
135
|
+
if json_mode:
|
|
136
|
+
print_json({"success": True, "data": {"hotel": hotel.to_dict()}})
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
click.echo(f"\n{'=' * 60}")
|
|
140
|
+
click.echo(f" {hotel.name}")
|
|
141
|
+
click.echo(f"{'=' * 60}")
|
|
142
|
+
click.echo(f" ID: {hotel.id or '—'}")
|
|
143
|
+
click.echo(f" Rating: {format_rating(hotel.rating, hotel.review_count)}")
|
|
144
|
+
click.echo(f" Price range: {hotel.price_range or '—'}")
|
|
145
|
+
click.echo(f" Address: {hotel.address or '—'}")
|
|
146
|
+
click.echo(f" City: {hotel.city or '—'}")
|
|
147
|
+
click.echo(f" Country: {hotel.country or '—'}")
|
|
148
|
+
click.echo(f" Telephone: {hotel.telephone or '—'}")
|
|
149
|
+
click.echo(f" Coordinates: {hotel.latitude or '?'}, {hotel.longitude or '?'}")
|
|
150
|
+
if hotel.amenities:
|
|
151
|
+
click.echo(f" Amenities: {', '.join(hotel.amenities[:10])}")
|
|
152
|
+
if len(hotel.amenities) > 10:
|
|
153
|
+
click.echo(f" ... and {len(hotel.amenities) - 10} more")
|
|
154
|
+
click.echo(f" URL: {hotel.url}")
|
|
155
|
+
click.echo()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Location search commands for cli-web-tripadvisor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..core.client import TripAdvisorClient
|
|
10
|
+
from ..utils.helpers import handle_errors, print_json, resolve_json_mode
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group("locations")
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def locations(ctx):
|
|
18
|
+
"""Search TripAdvisor destinations and locations."""
|
|
19
|
+
ctx.ensure_object(dict)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@locations.command("search")
|
|
23
|
+
@click.argument("query")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--max",
|
|
26
|
+
"max_results",
|
|
27
|
+
default=6,
|
|
28
|
+
type=int,
|
|
29
|
+
show_default=True,
|
|
30
|
+
help="Maximum number of results.",
|
|
31
|
+
)
|
|
32
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
33
|
+
@click.pass_context
|
|
34
|
+
def search_locations(ctx, query, max_results, json_mode):
|
|
35
|
+
"""Search for destinations matching QUERY.
|
|
36
|
+
|
|
37
|
+
Returns locations with their geo_id, which can be passed to
|
|
38
|
+
hotels/restaurants/attractions search via --geo-id.
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
|
|
42
|
+
cli-web-tripadvisor locations search "Paris"
|
|
43
|
+
|
|
44
|
+
cli-web-tripadvisor locations search "New York" --max 10
|
|
45
|
+
|
|
46
|
+
cli-web-tripadvisor locations search "Tokyo" --json
|
|
47
|
+
"""
|
|
48
|
+
json_mode = resolve_json_mode(json_mode, ctx)
|
|
49
|
+
|
|
50
|
+
with handle_errors(json_mode=json_mode):
|
|
51
|
+
with TripAdvisorClient() as client:
|
|
52
|
+
results = client.search_locations(query, max_results=max_results)
|
|
53
|
+
|
|
54
|
+
if json_mode:
|
|
55
|
+
print_json(
|
|
56
|
+
{
|
|
57
|
+
"success": True,
|
|
58
|
+
"data": {
|
|
59
|
+
"query": query,
|
|
60
|
+
"count": len(results),
|
|
61
|
+
"locations": [loc.to_dict() for loc in results],
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if not results:
|
|
68
|
+
click.echo(f"No locations found for '{query}'.")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
table = Table(
|
|
72
|
+
title=f"TripAdvisor Locations — {query}",
|
|
73
|
+
show_lines=False,
|
|
74
|
+
expand=False,
|
|
75
|
+
)
|
|
76
|
+
table.add_column("Geo ID", style="dim", no_wrap=True, max_width=10)
|
|
77
|
+
table.add_column("Type", max_width=12)
|
|
78
|
+
table.add_column("Name", max_width=50)
|
|
79
|
+
table.add_column("Region", max_width=30)
|
|
80
|
+
|
|
81
|
+
for loc in results:
|
|
82
|
+
table.add_row(
|
|
83
|
+
loc.geo_id,
|
|
84
|
+
loc.type,
|
|
85
|
+
loc.name,
|
|
86
|
+
loc.geo_name or loc.parent_name or "",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
console.print(table)
|
|
90
|
+
click.echo(f"\nFound {len(results)} location(s) for '{query}'.")
|
|
91
|
+
click.echo(
|
|
92
|
+
"Tip: Use --geo-id GEO_ID with hotels/restaurants/attractions search for faster results."
|
|
93
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Restaurant commands for cli-web-tripadvisor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..core.client import TripAdvisorClient
|
|
10
|
+
from ..utils.helpers import (
|
|
11
|
+
format_rating,
|
|
12
|
+
handle_errors,
|
|
13
|
+
print_json,
|
|
14
|
+
resolve_json_mode,
|
|
15
|
+
truncate,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group("restaurants")
|
|
22
|
+
@click.pass_context
|
|
23
|
+
def restaurants(ctx):
|
|
24
|
+
"""Search and browse TripAdvisor restaurants."""
|
|
25
|
+
ctx.ensure_object(dict)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@restaurants.command("search")
|
|
29
|
+
@click.argument("location")
|
|
30
|
+
@click.option(
|
|
31
|
+
"--geo-id", default=None, metavar="ID", help="Use known geo_id to skip location lookup."
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--page", default=1, type=int, show_default=True, help="Page number (30 restaurants per page)."
|
|
35
|
+
)
|
|
36
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
37
|
+
@click.pass_context
|
|
38
|
+
def search_restaurants(ctx, location, geo_id, page, json_mode):
|
|
39
|
+
"""Search restaurants in LOCATION.
|
|
40
|
+
|
|
41
|
+
LOCATION is a destination name like "Paris" or "New York City".
|
|
42
|
+
Use --geo-id to skip the location-lookup step (faster).
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
|
|
46
|
+
cli-web-tripadvisor restaurants search "Paris"
|
|
47
|
+
|
|
48
|
+
cli-web-tripadvisor restaurants search "Paris" --geo-id 187147
|
|
49
|
+
|
|
50
|
+
cli-web-tripadvisor restaurants search "Barcelona" --page 2
|
|
51
|
+
|
|
52
|
+
cli-web-tripadvisor restaurants search "Rome" --json
|
|
53
|
+
"""
|
|
54
|
+
json_mode = resolve_json_mode(json_mode, ctx)
|
|
55
|
+
|
|
56
|
+
with handle_errors(json_mode=json_mode):
|
|
57
|
+
with TripAdvisorClient() as client:
|
|
58
|
+
result = client.search_restaurants(location, geo_id=geo_id, page=page)
|
|
59
|
+
|
|
60
|
+
restlist = result.get("restaurants", [])
|
|
61
|
+
|
|
62
|
+
if json_mode:
|
|
63
|
+
print_json(
|
|
64
|
+
{
|
|
65
|
+
"success": True,
|
|
66
|
+
"data": {
|
|
67
|
+
"location": location,
|
|
68
|
+
"geo_id": result.get("geo_id"),
|
|
69
|
+
"page": page,
|
|
70
|
+
"count": len(restlist),
|
|
71
|
+
"restaurants": [r.to_dict() for r in restlist],
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
if not restlist:
|
|
78
|
+
click.echo(
|
|
79
|
+
f"No restaurants found for '{location}' (page {page}). "
|
|
80
|
+
"Try a different location name or --geo-id."
|
|
81
|
+
)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
table = Table(
|
|
85
|
+
title=f"Restaurants in {location} — page {page}",
|
|
86
|
+
show_lines=False,
|
|
87
|
+
expand=False,
|
|
88
|
+
)
|
|
89
|
+
table.add_column("ID", style="dim", no_wrap=True, max_width=10)
|
|
90
|
+
table.add_column("Name", max_width=36)
|
|
91
|
+
table.add_column("Rating", justify="right", max_width=14)
|
|
92
|
+
table.add_column("Price", justify="center", max_width=8)
|
|
93
|
+
table.add_column("Cuisines", max_width=22)
|
|
94
|
+
table.add_column("Phone", max_width=18)
|
|
95
|
+
|
|
96
|
+
for r in restlist:
|
|
97
|
+
cuisines = ", ".join(r.cuisines[:2]) if r.cuisines else "—"
|
|
98
|
+
table.add_row(
|
|
99
|
+
r.id,
|
|
100
|
+
truncate(r.name, 36),
|
|
101
|
+
format_rating(r.rating, r.review_count),
|
|
102
|
+
r.price_range or "—",
|
|
103
|
+
cuisines,
|
|
104
|
+
r.telephone or "—",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
console.print(table)
|
|
108
|
+
click.echo(f"\nShowing {len(restlist)} restaurant(s) on page {page}.")
|
|
109
|
+
if len(restlist) >= 30:
|
|
110
|
+
click.echo(f"Next page: restaurants search '{location}' --page {page + 1}")
|
|
111
|
+
click.echo("Tip: Use 'restaurants get URL' with the restaurant URL to see full details.")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@restaurants.command("get")
|
|
115
|
+
@click.argument("url")
|
|
116
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
117
|
+
@click.pass_context
|
|
118
|
+
def get_restaurant(ctx, url, json_mode):
|
|
119
|
+
"""Get detailed information for a restaurant by its TripAdvisor URL.
|
|
120
|
+
|
|
121
|
+
The URL is the full TripAdvisor restaurant URL from a search result,
|
|
122
|
+
e.g. https://www.tripadvisor.com/Restaurant_Review-g187147-d1035679-Reviews-...
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
|
|
126
|
+
cli-web-tripadvisor restaurants get "https://www.tripadvisor.com/Restaurant_Review-g187147-d1035679-Reviews-Da_Franco-Paris_Ile_de_France.html"
|
|
127
|
+
|
|
128
|
+
cli-web-tripadvisor restaurants get "https://www.tripadvisor.com/Restaurant_Review-..." --json
|
|
129
|
+
"""
|
|
130
|
+
json_mode = resolve_json_mode(json_mode, ctx)
|
|
131
|
+
|
|
132
|
+
with handle_errors(json_mode=json_mode):
|
|
133
|
+
with TripAdvisorClient() as client:
|
|
134
|
+
rest = client.get_restaurant(url)
|
|
135
|
+
|
|
136
|
+
if json_mode:
|
|
137
|
+
print_json({"success": True, "data": {"restaurant": rest.to_dict()}})
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
click.echo(f"\n{'=' * 60}")
|
|
141
|
+
click.echo(f" {rest.name}")
|
|
142
|
+
click.echo(f"{'=' * 60}")
|
|
143
|
+
click.echo(f" ID: {rest.id or '—'}")
|
|
144
|
+
click.echo(f" Rating: {format_rating(rest.rating, rest.review_count)}")
|
|
145
|
+
click.echo(f" Price range: {rest.price_range or '—'}")
|
|
146
|
+
if rest.cuisines:
|
|
147
|
+
click.echo(f" Cuisines: {', '.join(rest.cuisines)}")
|
|
148
|
+
click.echo(f" Address: {rest.address or '—'}")
|
|
149
|
+
click.echo(f" City: {rest.city or '—'}")
|
|
150
|
+
click.echo(f" Telephone: {rest.telephone or '—'}")
|
|
151
|
+
click.echo(f" Coordinates: {rest.latitude or '?'}, {rest.longitude or '?'}")
|
|
152
|
+
if rest.opening_hours:
|
|
153
|
+
click.echo(" Hours:")
|
|
154
|
+
for h in rest.opening_hours[:7]:
|
|
155
|
+
click.echo(f" {h}")
|
|
156
|
+
click.echo(f" URL: {rest.url}")
|
|
157
|
+
click.echo()
|