scrapebadger-cli 0.2.1__py3-none-any.whl
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.
- scrapebadger_cli/__init__.py +1 -0
- scrapebadger_cli/client.py +57 -0
- scrapebadger_cli/commands/__init__.py +1 -0
- scrapebadger_cli/commands/amazon.py +227 -0
- scrapebadger_cli/commands/auth.py +52 -0
- scrapebadger_cli/commands/ebay.py +217 -0
- scrapebadger_cli/commands/google.py +1117 -0
- scrapebadger_cli/commands/idealista.py +220 -0
- scrapebadger_cli/commands/immobiliare.py +151 -0
- scrapebadger_cli/commands/leboncoin.py +154 -0
- scrapebadger_cli/commands/loopnet.py +89 -0
- scrapebadger_cli/commands/realtor.py +91 -0
- scrapebadger_cli/commands/reddit.py +173 -0
- scrapebadger_cli/commands/stream.py +270 -0
- scrapebadger_cli/commands/tiktok.py +283 -0
- scrapebadger_cli/commands/twitter.py +235 -0
- scrapebadger_cli/commands/vinted.py +105 -0
- scrapebadger_cli/commands/web.py +117 -0
- scrapebadger_cli/commands/youtube.py +625 -0
- scrapebadger_cli/commands/zillow.py +105 -0
- scrapebadger_cli/config.py +69 -0
- scrapebadger_cli/main.py +52 -0
- scrapebadger_cli/output.py +116 -0
- scrapebadger_cli-0.2.1.dist-info/METADATA +41 -0
- scrapebadger_cli-0.2.1.dist-info/RECORD +27 -0
- scrapebadger_cli-0.2.1.dist-info/WHEEL +4 -0
- scrapebadger_cli-0.2.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Idealista real-estate commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from scrapebadger_cli.client import api_get
|
|
6
|
+
from scrapebadger_cli.output import render
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(no_args_is_help=True)
|
|
9
|
+
|
|
10
|
+
FMT = typer.Option("json", "--output", "-o", help="Output format: json, csv, table, markdown")
|
|
11
|
+
FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
|
|
12
|
+
MARKET = typer.Option("es", "--market", "-m", help="Idealista market: es, it or pt")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def suggest(
|
|
17
|
+
query: str = typer.Argument(..., help="Free-text location, e.g. 'sagrada familia'"),
|
|
18
|
+
operation: str = typer.Option("sale", "--operation", help="sale or rent"),
|
|
19
|
+
property_type: str = typer.Option("homes", "--property-type", help="e.g. homes, offices"),
|
|
20
|
+
market: str = MARKET,
|
|
21
|
+
locale: str | None = typer.Option(None, "--locale", help="Response locale: en, es, it or pt"),
|
|
22
|
+
fmt: str = FMT,
|
|
23
|
+
fields: str | None = FIELDS,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Resolve free-text location text into Idealista location codes."""
|
|
26
|
+
data = api_get(
|
|
27
|
+
"/v1/idealista/suggest",
|
|
28
|
+
{
|
|
29
|
+
"query": query,
|
|
30
|
+
"operation": operation,
|
|
31
|
+
"property_type": property_type,
|
|
32
|
+
"market": market,
|
|
33
|
+
"locale": locale,
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
render(data, fmt, fields)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command()
|
|
40
|
+
def search(
|
|
41
|
+
location: str = typer.Argument(..., help="Idealista location code (from suggest)"),
|
|
42
|
+
operation: str = typer.Option("sale", "--operation", help="sale or rent"),
|
|
43
|
+
property_type: str = typer.Option("homes", "--property-type", help="e.g. homes, offices"),
|
|
44
|
+
market: str = MARKET,
|
|
45
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
46
|
+
max_items: int = typer.Option(30, "--max-items", help="Results per page (1..50)"),
|
|
47
|
+
sort_by: str | None = typer.Option(
|
|
48
|
+
None,
|
|
49
|
+
"--sort-by",
|
|
50
|
+
help="distance, size, rooms, floor, ratioeurm2, price, street, photos, "
|
|
51
|
+
"modificationDate, publicationDate, weigh, priceDown, "
|
|
52
|
+
"preservationTypeAndPrice or privateAds",
|
|
53
|
+
),
|
|
54
|
+
sort_order: str = typer.Option("desc", "--sort-order", help="asc or desc"),
|
|
55
|
+
min_price: float | None = typer.Option(None, "--min-price"),
|
|
56
|
+
max_price: float | None = typer.Option(None, "--max-price"),
|
|
57
|
+
min_size: float | None = typer.Option(None, "--min-size"),
|
|
58
|
+
max_size: float | None = typer.Option(None, "--max-size"),
|
|
59
|
+
min_rooms: int | None = typer.Option(None, "--min-rooms"),
|
|
60
|
+
max_rooms: int | None = typer.Option(None, "--max-rooms"),
|
|
61
|
+
locale: str | None = typer.Option(None, "--locale", help="Response locale: en, es, it or pt"),
|
|
62
|
+
fmt: str = FMT,
|
|
63
|
+
fields: str | None = FIELDS,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Search Idealista property listings by location code."""
|
|
66
|
+
data = api_get(
|
|
67
|
+
"/v1/idealista/search",
|
|
68
|
+
{
|
|
69
|
+
"location": location,
|
|
70
|
+
"operation": operation,
|
|
71
|
+
"property_type": property_type,
|
|
72
|
+
"market": market,
|
|
73
|
+
"page": page,
|
|
74
|
+
"max_items": max_items,
|
|
75
|
+
"sort_by": sort_by,
|
|
76
|
+
"sort_order": sort_order,
|
|
77
|
+
"min_price": min_price,
|
|
78
|
+
"max_price": max_price,
|
|
79
|
+
"min_size": min_size,
|
|
80
|
+
"max_size": max_size,
|
|
81
|
+
"min_rooms": min_rooms,
|
|
82
|
+
"max_rooms": max_rooms,
|
|
83
|
+
"locale": locale,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
render(data, fmt, fields)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command(name="search-all")
|
|
90
|
+
def search_all(
|
|
91
|
+
location: str = typer.Argument(..., help="Idealista location code (from suggest)"),
|
|
92
|
+
operation: str = typer.Option("sale", "--operation", help="sale or rent"),
|
|
93
|
+
property_type: str = typer.Option("homes", "--property-type", help="e.g. homes, offices"),
|
|
94
|
+
market: str = MARKET,
|
|
95
|
+
max_results: int = typer.Option(500, "--max-results", help="Max deduped results (1..3000)"),
|
|
96
|
+
min_price: float | None = typer.Option(None, "--min-price"),
|
|
97
|
+
max_price: float | None = typer.Option(None, "--max-price"),
|
|
98
|
+
min_size: float | None = typer.Option(None, "--min-size"),
|
|
99
|
+
max_size: float | None = typer.Option(None, "--max-size"),
|
|
100
|
+
min_rooms: int | None = typer.Option(None, "--min-rooms"),
|
|
101
|
+
max_rooms: int | None = typer.Option(None, "--max-rooms"),
|
|
102
|
+
locale: str | None = typer.Option(None, "--locale", help="Response locale: en, es, it or pt"),
|
|
103
|
+
fmt: str = FMT,
|
|
104
|
+
fields: str | None = FIELDS,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Fetch the full Idealista inventory for a location via price tiling."""
|
|
107
|
+
data = api_get(
|
|
108
|
+
"/v1/idealista/search/all",
|
|
109
|
+
{
|
|
110
|
+
"location": location,
|
|
111
|
+
"operation": operation,
|
|
112
|
+
"property_type": property_type,
|
|
113
|
+
"market": market,
|
|
114
|
+
"max_results": max_results,
|
|
115
|
+
"min_price": min_price,
|
|
116
|
+
"max_price": max_price,
|
|
117
|
+
"min_size": min_size,
|
|
118
|
+
"max_size": max_size,
|
|
119
|
+
"min_rooms": min_rooms,
|
|
120
|
+
"max_rooms": max_rooms,
|
|
121
|
+
"locale": locale,
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
render(data, fmt, fields)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command()
|
|
128
|
+
def property(
|
|
129
|
+
property_code: str = typer.Argument(..., help="Idealista property code"),
|
|
130
|
+
market: str = MARKET,
|
|
131
|
+
locale: str = typer.Option("en", "--locale", help="Response locale: en, es, it or pt"),
|
|
132
|
+
fmt: str = FMT,
|
|
133
|
+
fields: str | None = FIELDS,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Get the full detail of a single Idealista property by its code."""
|
|
136
|
+
data = api_get(
|
|
137
|
+
f"/v1/idealista/properties/{property_code}",
|
|
138
|
+
{"market": market, "locale": locale},
|
|
139
|
+
)
|
|
140
|
+
render(data, fmt, fields)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.command()
|
|
144
|
+
def stats(
|
|
145
|
+
property_code: str = typer.Argument(..., help="Idealista property code"),
|
|
146
|
+
market: str = MARKET,
|
|
147
|
+
locale: str = typer.Option("en", "--locale", help="Response locale: en, es, it or pt"),
|
|
148
|
+
fmt: str = FMT,
|
|
149
|
+
fields: str | None = FIELDS,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Get engagement stats (views, contacts, favorites) for an Idealista listing."""
|
|
152
|
+
data = api_get(
|
|
153
|
+
f"/v1/idealista/properties/{property_code}/stats",
|
|
154
|
+
{"market": market, "locale": locale},
|
|
155
|
+
)
|
|
156
|
+
render(data, fmt, fields)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command()
|
|
160
|
+
def agency(
|
|
161
|
+
short_name: str = typer.Argument(..., help="Agency URL slug (micrositeShortName)"),
|
|
162
|
+
market: str = MARKET,
|
|
163
|
+
operation: str | None = typer.Option(None, "--operation", help="sale or rent"),
|
|
164
|
+
property_type: str | None = typer.Option(None, "--property-type", help="e.g. homes, offices"),
|
|
165
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
166
|
+
max_items: int = typer.Option(30, "--max-items", help="Listings per page (1..50)"),
|
|
167
|
+
include_listings: bool = typer.Option(True, "--include-listings/--no-listings"),
|
|
168
|
+
fmt: str = FMT,
|
|
169
|
+
fields: str | None = FIELDS,
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Get an Idealista agency (microsite) profile plus a page of its listings."""
|
|
172
|
+
data = api_get(
|
|
173
|
+
f"/v1/idealista/agency/{short_name}",
|
|
174
|
+
{
|
|
175
|
+
"market": market,
|
|
176
|
+
"operation": operation,
|
|
177
|
+
"property_type": property_type,
|
|
178
|
+
"page": page,
|
|
179
|
+
"max_items": max_items,
|
|
180
|
+
"include_listings": include_listings,
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
render(data, fmt, fields)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.command(name="agency-by-phone")
|
|
187
|
+
def agency_by_phone(
|
|
188
|
+
phone: str = typer.Argument(..., help="Agency national contact number (no country prefix)"),
|
|
189
|
+
market: str = MARKET,
|
|
190
|
+
operation: str | None = typer.Option(None, "--operation", help="sale or rent"),
|
|
191
|
+
property_type: str | None = typer.Option(None, "--property-type", help="e.g. homes, offices"),
|
|
192
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
193
|
+
max_items: int = typer.Option(30, "--max-items", help="Listings per page (1..50)"),
|
|
194
|
+
include_listings: bool = typer.Option(True, "--include-listings/--no-listings"),
|
|
195
|
+
fmt: str = FMT,
|
|
196
|
+
fields: str | None = FIELDS,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Reverse-lookup an Idealista agency from its contact phone number."""
|
|
199
|
+
data = api_get(
|
|
200
|
+
f"/v1/idealista/agency/by-phone/{phone}",
|
|
201
|
+
{
|
|
202
|
+
"market": market,
|
|
203
|
+
"operation": operation,
|
|
204
|
+
"property_type": property_type,
|
|
205
|
+
"page": page,
|
|
206
|
+
"max_items": max_items,
|
|
207
|
+
"include_listings": include_listings,
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
render(data, fmt, fields)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@app.command()
|
|
214
|
+
def markets(
|
|
215
|
+
fmt: str = FMT,
|
|
216
|
+
fields: str | None = FIELDS,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""List all supported Idealista markets (es, it, pt)."""
|
|
219
|
+
data = api_get("/v1/idealista/markets")
|
|
220
|
+
render(data, fmt, fields)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Immobiliare.it real-estate commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from scrapebadger_cli.client import api_get
|
|
6
|
+
from scrapebadger_cli.output import render
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(no_args_is_help=True)
|
|
9
|
+
|
|
10
|
+
FMT = typer.Option("json", "--output", "-o", help="Output format: json, csv, table, markdown")
|
|
11
|
+
FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
|
|
12
|
+
MARKET = typer.Option("it", "--market", "-m", help="Market: it, es, gr, lu")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def autocomplete(
|
|
17
|
+
query: str = typer.Argument(..., help="Partial location text"),
|
|
18
|
+
market: str = MARKET,
|
|
19
|
+
fmt: str = FMT,
|
|
20
|
+
fields: str | None = FIELDS,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Get Immobiliare location autocomplete suggestions."""
|
|
23
|
+
data = api_get("/v1/immobiliare/autocomplete", {"query": query, "market": market})
|
|
24
|
+
render(data, fmt, fields)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
def search(
|
|
29
|
+
location: str | None = typer.Option(None, "--location", "-l", help="Free-text location"),
|
|
30
|
+
region_id: str | None = typer.Option(None, "--region-id", help="Region ID"),
|
|
31
|
+
province_id: str | None = typer.Option(None, "--province-id", help="Province ID"),
|
|
32
|
+
city_id: str | None = typer.Option(None, "--city-id", help="City ID"),
|
|
33
|
+
contract: str | None = typer.Option(None, "--contract", help="sale, rent"),
|
|
34
|
+
category: str | None = typer.Option(
|
|
35
|
+
None,
|
|
36
|
+
"--category",
|
|
37
|
+
help="residential, commercial, garages, offices, land, buildings, warehouses",
|
|
38
|
+
),
|
|
39
|
+
price_min: int | None = typer.Option(None, "--price-min"),
|
|
40
|
+
price_max: int | None = typer.Option(None, "--price-max"),
|
|
41
|
+
surface_min: int | None = typer.Option(None, "--surface-min", help="Min surface (m²)"),
|
|
42
|
+
surface_max: int | None = typer.Option(None, "--surface-max", help="Max surface (m²)"),
|
|
43
|
+
rooms_min: int | None = typer.Option(None, "--rooms-min"),
|
|
44
|
+
rooms_max: int | None = typer.Option(None, "--rooms-max"),
|
|
45
|
+
bathrooms_min: int | None = typer.Option(None, "--bathrooms-min"),
|
|
46
|
+
sort: str | None = typer.Option(
|
|
47
|
+
None,
|
|
48
|
+
"--sort",
|
|
49
|
+
help="relevance, price_asc, price_desc, newest, oldest, surface_desc, surface_asc",
|
|
50
|
+
),
|
|
51
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
52
|
+
market: str = MARKET,
|
|
53
|
+
fmt: str = FMT,
|
|
54
|
+
fields: str | None = FIELDS,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Search Immobiliare real-estate listings."""
|
|
57
|
+
data = api_get(
|
|
58
|
+
"/v1/immobiliare/search",
|
|
59
|
+
{
|
|
60
|
+
"market": market,
|
|
61
|
+
"location": location,
|
|
62
|
+
"region_id": region_id,
|
|
63
|
+
"province_id": province_id,
|
|
64
|
+
"city_id": city_id,
|
|
65
|
+
"contract": contract,
|
|
66
|
+
"category": category,
|
|
67
|
+
"price_min": price_min,
|
|
68
|
+
"price_max": price_max,
|
|
69
|
+
"surface_min": surface_min,
|
|
70
|
+
"surface_max": surface_max,
|
|
71
|
+
"rooms_min": rooms_min,
|
|
72
|
+
"rooms_max": rooms_max,
|
|
73
|
+
"bathrooms_min": bathrooms_min,
|
|
74
|
+
"sort": sort,
|
|
75
|
+
"page": page,
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
render(data, fmt, fields)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command()
|
|
82
|
+
def listing(
|
|
83
|
+
listing_id: int = typer.Argument(..., help="Immobiliare listing ID"),
|
|
84
|
+
market: str = MARKET,
|
|
85
|
+
fmt: str = FMT,
|
|
86
|
+
fields: str | None = FIELDS,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Get a full Immobiliare listing detail by ID."""
|
|
89
|
+
data = api_get(f"/v1/immobiliare/listings/{listing_id}", {"market": market})
|
|
90
|
+
render(data, fmt, fields)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command()
|
|
94
|
+
def agency(
|
|
95
|
+
agency_id: int = typer.Argument(..., help="Immobiliare agency ID"),
|
|
96
|
+
market: str = MARKET,
|
|
97
|
+
fmt: str = FMT,
|
|
98
|
+
fields: str | None = FIELDS,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Get an Immobiliare agency profile by ID."""
|
|
101
|
+
data = api_get(f"/v1/immobiliare/agencies/{agency_id}", {"market": market})
|
|
102
|
+
render(data, fmt, fields)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command(name="agency-listings")
|
|
106
|
+
def agency_listings(
|
|
107
|
+
agency_id: int = typer.Argument(..., help="Immobiliare agency ID"),
|
|
108
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
109
|
+
market: str = MARKET,
|
|
110
|
+
fmt: str = FMT,
|
|
111
|
+
fields: str | None = FIELDS,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Get the active listings of an Immobiliare agency."""
|
|
114
|
+
data = api_get(
|
|
115
|
+
f"/v1/immobiliare/agencies/{agency_id}/listings", {"market": market, "page": page}
|
|
116
|
+
)
|
|
117
|
+
render(data, fmt, fields)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command(name="price-insights")
|
|
121
|
+
def price_insights(
|
|
122
|
+
region_id: str = typer.Option(..., "--region-id", help="Region ID (required)"),
|
|
123
|
+
province_id: str | None = typer.Option(None, "--province-id", help="Narrow to a province"),
|
|
124
|
+
city_id: str | None = typer.Option(None, "--city-id", help="Narrow to a city"),
|
|
125
|
+
contract: str | None = typer.Option(None, "--contract", help="sale, rent"),
|
|
126
|
+
market: str = MARKET,
|
|
127
|
+
fmt: str = FMT,
|
|
128
|
+
fields: str | None = FIELDS,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Get price-per-m² history (monthly EUR/m² time series) for an area."""
|
|
131
|
+
data = api_get(
|
|
132
|
+
"/v1/immobiliare/market-insights/prices",
|
|
133
|
+
{
|
|
134
|
+
"market": market,
|
|
135
|
+
"region_id": region_id,
|
|
136
|
+
"province_id": province_id,
|
|
137
|
+
"city_id": city_id,
|
|
138
|
+
"contract": contract,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
render(data, fmt, fields)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.command()
|
|
145
|
+
def markets(
|
|
146
|
+
fmt: str = FMT,
|
|
147
|
+
fields: str | None = FIELDS,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""List supported Immobiliare Group markets (it, es, gr, lu)."""
|
|
150
|
+
data = api_get("/v1/immobiliare/markets")
|
|
151
|
+
render(data, fmt, fields)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Leboncoin classifieds commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from scrapebadger_cli.client import api_get
|
|
6
|
+
from scrapebadger_cli.output import render
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(no_args_is_help=True)
|
|
9
|
+
|
|
10
|
+
FMT = typer.Option("json", "--output", "-o", help="Output format: json, csv, table, markdown")
|
|
11
|
+
FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def search(
|
|
16
|
+
text: str | None = typer.Option(None, "--text", "-t", help="Search text / keywords"),
|
|
17
|
+
category: str | None = typer.Option(None, "--category", help="Restrict to a category"),
|
|
18
|
+
region_id: str | None = typer.Option(None, "--region-id", help="Restrict to a region"),
|
|
19
|
+
department_id: str | None = typer.Option(
|
|
20
|
+
None, "--department-id", help="Restrict to a department"
|
|
21
|
+
),
|
|
22
|
+
city: str | None = typer.Option(None, "--city", help="Restrict to a city"),
|
|
23
|
+
zipcode: str | None = typer.Option(None, "--zipcode", help="Restrict to a zip/postal code"),
|
|
24
|
+
price_min: int | None = typer.Option(None, "--price-min"),
|
|
25
|
+
price_max: int | None = typer.Option(None, "--price-max"),
|
|
26
|
+
owner_type: str | None = typer.Option(None, "--owner-type", help="all, pro, private"),
|
|
27
|
+
ad_type: str | None = typer.Option(None, "--ad-type", help="offer, demand"),
|
|
28
|
+
sort: str | None = typer.Option(
|
|
29
|
+
None, "--sort", help="relevance, newest, oldest, price_low, price_high"
|
|
30
|
+
),
|
|
31
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
32
|
+
limit: int | None = typer.Option(None, "--limit", help="Results per page (max 100)"),
|
|
33
|
+
fmt: str = FMT,
|
|
34
|
+
fields: str | None = FIELDS,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Search Leboncoin classified ads (France)."""
|
|
37
|
+
data = api_get(
|
|
38
|
+
"/v1/leboncoin/search",
|
|
39
|
+
{
|
|
40
|
+
"text": text,
|
|
41
|
+
"category": category,
|
|
42
|
+
"region_id": region_id,
|
|
43
|
+
"department_id": department_id,
|
|
44
|
+
"city": city,
|
|
45
|
+
"zipcode": zipcode,
|
|
46
|
+
"price_min": price_min,
|
|
47
|
+
"price_max": price_max,
|
|
48
|
+
"owner_type": owner_type,
|
|
49
|
+
"ad_type": ad_type,
|
|
50
|
+
"sort": sort,
|
|
51
|
+
"page": page,
|
|
52
|
+
"limit": limit,
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
render(data, fmt, fields)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command()
|
|
59
|
+
def ad(
|
|
60
|
+
list_id: int = typer.Argument(..., help="Leboncoin ad list ID"),
|
|
61
|
+
fmt: str = FMT,
|
|
62
|
+
fields: str | None = FIELDS,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Get a full Leboncoin ad detail page by list ID."""
|
|
65
|
+
data = api_get(f"/v1/leboncoin/ads/{list_id}")
|
|
66
|
+
render(data, fmt, fields)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command()
|
|
70
|
+
def similar(
|
|
71
|
+
list_id: int = typer.Argument(..., help="Leboncoin ad list ID"),
|
|
72
|
+
limit: int | None = typer.Option(None, "--limit", help="Max similar ads to return"),
|
|
73
|
+
fmt: str = FMT,
|
|
74
|
+
fields: str | None = FIELDS,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Get ads similar to a Leboncoin ad."""
|
|
77
|
+
data = api_get(f"/v1/leboncoin/ads/{list_id}/similar", {"limit": limit})
|
|
78
|
+
render(data, fmt, fields)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command()
|
|
82
|
+
def seller(
|
|
83
|
+
user_id: str = typer.Argument(..., help="Leboncoin seller UUID"),
|
|
84
|
+
fmt: str = FMT,
|
|
85
|
+
fields: str | None = FIELDS,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Get a Leboncoin seller / pro-store profile."""
|
|
88
|
+
data = api_get(f"/v1/leboncoin/sellers/{user_id}")
|
|
89
|
+
render(data, fmt, fields)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.command(name="seller-listings")
|
|
93
|
+
def seller_listings(
|
|
94
|
+
user_id: str = typer.Argument(..., help="Leboncoin seller UUID"),
|
|
95
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
96
|
+
limit: int | None = typer.Option(None, "--limit", help="Results per page (max 100)"),
|
|
97
|
+
fmt: str = FMT,
|
|
98
|
+
fields: str | None = FIELDS,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Get the active listings of a Leboncoin seller."""
|
|
101
|
+
data = api_get(f"/v1/leboncoin/sellers/{user_id}/listings", {"page": page, "limit": limit})
|
|
102
|
+
render(data, fmt, fields)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command()
|
|
106
|
+
def categories(
|
|
107
|
+
fmt: str = FMT,
|
|
108
|
+
fields: str | None = FIELDS,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""List reference Leboncoin categories."""
|
|
111
|
+
data = api_get("/v1/leboncoin/categories")
|
|
112
|
+
render(data, fmt, fields)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command()
|
|
116
|
+
def regions(
|
|
117
|
+
fmt: str = FMT,
|
|
118
|
+
fields: str | None = FIELDS,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""List reference Leboncoin regions."""
|
|
121
|
+
data = api_get("/v1/leboncoin/regions")
|
|
122
|
+
render(data, fmt, fields)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command()
|
|
126
|
+
def departments(
|
|
127
|
+
region_id: str | None = typer.Option(None, "--region-id", help="Restrict to a region"),
|
|
128
|
+
fmt: str = FMT,
|
|
129
|
+
fields: str | None = FIELDS,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""List reference Leboncoin departments (optionally scoped to a region)."""
|
|
132
|
+
data = api_get("/v1/leboncoin/departments", {"region_id": region_id})
|
|
133
|
+
render(data, fmt, fields)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command()
|
|
137
|
+
def locations(
|
|
138
|
+
q: str = typer.Argument(..., help="Partial location text"),
|
|
139
|
+
fmt: str = FMT,
|
|
140
|
+
fields: str | None = FIELDS,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Get Leboncoin location autocomplete suggestions."""
|
|
143
|
+
data = api_get("/v1/leboncoin/locations/search", {"q": q})
|
|
144
|
+
render(data, fmt, fields)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.command()
|
|
148
|
+
def markets(
|
|
149
|
+
fmt: str = FMT,
|
|
150
|
+
fields: str | None = FIELDS,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""List all supported Leboncoin markets (France only)."""
|
|
153
|
+
data = api_get("/v1/leboncoin/markets")
|
|
154
|
+
render(data, fmt, fields)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""LoopNet commercial real-estate commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from scrapebadger_cli.client import api_get
|
|
6
|
+
from scrapebadger_cli.output import render
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(no_args_is_help=True)
|
|
9
|
+
|
|
10
|
+
FMT = typer.Option("json", "--output", "-o", help="Output format: json, csv, table, markdown")
|
|
11
|
+
FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
|
|
12
|
+
MARKET = typer.Option("us", "--market", "-m", help="us, ca, uk, fr, es")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def search(
|
|
17
|
+
location: str = typer.Argument(..., help='City/state, ZIP, state code or "usa"'),
|
|
18
|
+
market: str = MARKET,
|
|
19
|
+
listing_type: str = typer.Option(
|
|
20
|
+
"for-lease", "--listing-type", "-t", help="for-lease, for-sale, auctions"
|
|
21
|
+
),
|
|
22
|
+
property_type: str | None = typer.Option(
|
|
23
|
+
None, "--property-type", help="Slug from `loopnet property-types` (default all)"
|
|
24
|
+
),
|
|
25
|
+
page: int = typer.Option(1, "--page", "-p", help="1-20 (LoopNet caps at ~500 results)"),
|
|
26
|
+
min_price: int | None = typer.Option(None, "--min-price"),
|
|
27
|
+
max_price: int | None = typer.Option(None, "--max-price"),
|
|
28
|
+
price_type: str | None = typer.Option(None, "--price-type", help="unit, sf, acre"),
|
|
29
|
+
min_size: int | None = typer.Option(None, "--min-size", help="Minimum size (SF)"),
|
|
30
|
+
max_size: int | None = typer.Option(None, "--max-size", help="Maximum size (SF)"),
|
|
31
|
+
fmt: str = FMT,
|
|
32
|
+
fields: str | None = FIELDS,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Search LoopNet for commercial real-estate listings."""
|
|
35
|
+
data = api_get(
|
|
36
|
+
"/v1/loopnet/search",
|
|
37
|
+
{
|
|
38
|
+
"location": location,
|
|
39
|
+
"market": market,
|
|
40
|
+
"listing_type": listing_type,
|
|
41
|
+
"property_type": property_type,
|
|
42
|
+
"page": page,
|
|
43
|
+
"min_price": min_price,
|
|
44
|
+
"max_price": max_price,
|
|
45
|
+
"price_type": price_type,
|
|
46
|
+
"min_size": min_size,
|
|
47
|
+
"max_size": max_size,
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
render(data, fmt, fields)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command()
|
|
54
|
+
def listing(
|
|
55
|
+
listing_id: str = typer.Argument(..., help="LoopNet listing id"),
|
|
56
|
+
market: str = MARKET,
|
|
57
|
+
fmt: str = FMT,
|
|
58
|
+
fields: str | None = FIELDS,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Get a LoopNet listing's full detail by listing id."""
|
|
61
|
+
data = api_get(f"/v1/loopnet/listings/{listing_id}", {"market": market})
|
|
62
|
+
render(data, fmt, fields)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command()
|
|
66
|
+
def broker(
|
|
67
|
+
slug: str = typer.Argument(..., help="Broker URL slug"),
|
|
68
|
+
broker_id: str = typer.Argument(..., help="LoopNet broker id"),
|
|
69
|
+
market: str = MARKET,
|
|
70
|
+
fmt: str = FMT,
|
|
71
|
+
fields: str | None = FIELDS,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Get a LoopNet broker's profile and their active listings."""
|
|
74
|
+
data = api_get(f"/v1/loopnet/brokers/{slug}/{broker_id}", {"market": market})
|
|
75
|
+
render(data, fmt, fields)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command("markets")
|
|
79
|
+
def list_markets(fmt: str = FMT, fields: str | None = FIELDS) -> None:
|
|
80
|
+
"""List LoopNet coverage markets (US, CA, UK, FR, ES)."""
|
|
81
|
+
data = api_get("/v1/loopnet/markets", {})
|
|
82
|
+
render(data, fmt, fields)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command("property-types")
|
|
86
|
+
def list_property_types(fmt: str = FMT, fields: str | None = FIELDS) -> None:
|
|
87
|
+
"""List LoopNet property-type slugs (office, retail, industrial, etc.)."""
|
|
88
|
+
data = api_get("/v1/loopnet/property-types", {})
|
|
89
|
+
render(data, fmt, fields)
|