Add debug CLI listings subcommand (list, detail, stream, refresh)
This commit is contained in:
parent
34f9e933c0
commit
34743127fb
2 changed files with 235 additions and 0 deletions
233
cli/listings.py
Normal file
233
cli/listings.py
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
"""Debug CLI — listing commands."""
|
||||
import asyncio
|
||||
|
||||
import click
|
||||
import httpx
|
||||
|
||||
from cli._context import CliContext, get_http_headers, output, error_output
|
||||
from database import engine
|
||||
from models.listing import ListingType, QueryParameters
|
||||
from repositories.listing_repository import ListingRepository
|
||||
from services import listing_service, export_service
|
||||
|
||||
|
||||
@click.group("listings")
|
||||
def listings_group() -> None:
|
||||
"""Listing browsing and management commands."""
|
||||
pass
|
||||
|
||||
|
||||
@listings_group.command("list")
|
||||
@click.option("--type", "-t", "listing_type", default="RENT", type=click.Choice(["RENT", "BUY"]))
|
||||
@click.option("--limit", default=10, type=int, help="Max listings to return")
|
||||
@click.pass_context
|
||||
def list_listings(ctx: click.Context, listing_type: str, limit: int) -> None:
|
||||
"""List listings from the database."""
|
||||
cli_ctx: CliContext = ctx.obj["cli_ctx"]
|
||||
|
||||
try:
|
||||
if cli_ctx.use_http:
|
||||
resp = httpx.get(
|
||||
f"{cli_ctx.api_base_url}/api/listing",
|
||||
headers=get_http_headers(cli_ctx.user_email),
|
||||
params={"limit": limit},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
else:
|
||||
repository = ListingRepository(engine=engine)
|
||||
result = asyncio.run(listing_service.get_listings(repository, limit=limit))
|
||||
data = {
|
||||
"total_count": result.total_count,
|
||||
"listings": [
|
||||
{
|
||||
"id": listing.id,
|
||||
"price": listing.price,
|
||||
"bedrooms": listing.number_of_bedrooms,
|
||||
"sqm": listing.square_meters,
|
||||
"url": listing.url,
|
||||
}
|
||||
for listing in result.listings
|
||||
],
|
||||
}
|
||||
|
||||
output(data, cli_ctx.json_output)
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_output(f"HTTP {e.response.status_code}: {e.response.text}", cli_ctx.json_output)
|
||||
except Exception as e:
|
||||
error_output(str(e), cli_ctx.json_output)
|
||||
|
||||
|
||||
@listings_group.command("detail")
|
||||
@click.argument("listing_id", type=int)
|
||||
@click.option("--type", "-t", "listing_type", default="RENT", type=click.Choice(["RENT", "BUY"]))
|
||||
@click.pass_context
|
||||
def listing_detail(ctx: click.Context, listing_id: int, listing_type: str) -> None:
|
||||
"""Get detailed information for a single listing."""
|
||||
cli_ctx: CliContext = ctx.obj["cli_ctx"]
|
||||
|
||||
try:
|
||||
if cli_ctx.use_http:
|
||||
resp = httpx.get(
|
||||
f"{cli_ctx.api_base_url}/api/listing/{listing_id}/detail",
|
||||
headers=get_http_headers(cli_ctx.user_email),
|
||||
params={"listing_type": listing_type},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
else:
|
||||
repository = ListingRepository(engine=engine)
|
||||
lt = ListingType(listing_type)
|
||||
listings = asyncio.run(repository.get_listings(only_ids=[listing_id], listing_type=lt))
|
||||
if not listings:
|
||||
error_output("Listing not found", cli_ctx.json_output)
|
||||
return
|
||||
listing = listings[0]
|
||||
data = {
|
||||
"id": listing.id,
|
||||
"price": listing.price,
|
||||
"bedrooms": listing.number_of_bedrooms,
|
||||
"sqm": listing.square_meters,
|
||||
"agency": listing.agency,
|
||||
"url": listing.url,
|
||||
"council_tax_band": listing.council_tax_band,
|
||||
}
|
||||
|
||||
output(data, cli_ctx.json_output)
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_output(f"HTTP {e.response.status_code}: {e.response.text}", cli_ctx.json_output)
|
||||
except Exception as e:
|
||||
error_output(str(e), cli_ctx.json_output)
|
||||
|
||||
|
||||
@listings_group.command("stream")
|
||||
@click.option("--type", "-t", "listing_type", default="RENT", type=click.Choice(["RENT", "BUY"]))
|
||||
@click.option("--batch-size", default=50, type=int)
|
||||
@click.option("--limit", default=None, type=int)
|
||||
@click.option("--min-bedrooms", default=1, type=int)
|
||||
@click.option("--max-bedrooms", default=999, type=int)
|
||||
@click.option("--min-price", default=0, type=int)
|
||||
@click.option("--max-price", default=10_000_000, type=int)
|
||||
@click.pass_context
|
||||
def stream_listings(
|
||||
ctx: click.Context,
|
||||
listing_type: str,
|
||||
batch_size: int,
|
||||
limit: int | None,
|
||||
min_bedrooms: int,
|
||||
max_bedrooms: int,
|
||||
min_price: int,
|
||||
max_price: int,
|
||||
) -> None:
|
||||
"""Stream listings as NDJSON (mirrors the streaming API endpoint)."""
|
||||
cli_ctx: CliContext = ctx.obj["cli_ctx"]
|
||||
lt = ListingType[listing_type]
|
||||
qp = QueryParameters(
|
||||
listing_type=lt,
|
||||
min_bedrooms=min_bedrooms,
|
||||
max_bedrooms=max_bedrooms,
|
||||
min_price=min_price,
|
||||
max_price=max_price,
|
||||
)
|
||||
|
||||
try:
|
||||
if cli_ctx.use_http:
|
||||
params: dict[str, int | str] = {
|
||||
"listing_type": listing_type,
|
||||
"batch_size": batch_size,
|
||||
"min_bedrooms": min_bedrooms,
|
||||
"max_bedrooms": max_bedrooms,
|
||||
"min_price": min_price,
|
||||
"max_price": max_price,
|
||||
}
|
||||
if limit is not None:
|
||||
params["limit"] = limit
|
||||
with httpx.stream(
|
||||
"GET",
|
||||
f"{cli_ctx.api_base_url}/api/listing_geojson/stream",
|
||||
headers=get_http_headers(cli_ctx.user_email),
|
||||
params=params,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
for line in resp.iter_lines():
|
||||
if line.strip():
|
||||
print(line)
|
||||
else:
|
||||
result = asyncio.run(
|
||||
export_service.export_to_geojson(
|
||||
repository=ListingRepository(engine=engine),
|
||||
query_parameters=qp,
|
||||
limit=limit,
|
||||
)
|
||||
)
|
||||
if cli_ctx.json_output:
|
||||
output(result.data, json_mode=True)
|
||||
else:
|
||||
features = result.data.get("features", []) if result.data else []
|
||||
print(f"Streamed {len(features)} features")
|
||||
for f in features[:5]:
|
||||
props = f.get("properties", {})
|
||||
print(f" [{props.get('id')}] {props.get('displayAddress', 'N/A')} - £{props.get('price', '?')}")
|
||||
if len(features) > 5:
|
||||
print(f" ... and {len(features) - 5} more")
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_output(f"HTTP {e.response.status_code}: {e.response.text}", cli_ctx.json_output)
|
||||
except Exception as e:
|
||||
error_output(str(e), cli_ctx.json_output)
|
||||
|
||||
|
||||
@listings_group.command("refresh")
|
||||
@click.option("--type", "-t", "listing_type", required=True, type=click.Choice(["RENT", "BUY"]))
|
||||
@click.option("--min-bedrooms", default=1, type=int)
|
||||
@click.option("--max-bedrooms", default=10, type=int)
|
||||
@click.option("--min-price", default=0, type=int)
|
||||
@click.option("--max-price", default=999_999, type=int)
|
||||
@click.pass_context
|
||||
def refresh_listings(
|
||||
ctx: click.Context,
|
||||
listing_type: str,
|
||||
min_bedrooms: int,
|
||||
max_bedrooms: int,
|
||||
min_price: int,
|
||||
max_price: int,
|
||||
) -> None:
|
||||
"""Trigger a listing refresh (scrape from Rightmove)."""
|
||||
cli_ctx: CliContext = ctx.obj["cli_ctx"]
|
||||
lt = ListingType[listing_type]
|
||||
qp = QueryParameters(
|
||||
listing_type=lt,
|
||||
min_bedrooms=min_bedrooms,
|
||||
max_bedrooms=max_bedrooms,
|
||||
min_price=min_price,
|
||||
max_price=max_price,
|
||||
)
|
||||
|
||||
try:
|
||||
if cli_ctx.use_http:
|
||||
resp = httpx.post(
|
||||
f"{cli_ctx.api_base_url}/api/refresh_listings",
|
||||
headers=get_http_headers(cli_ctx.user_email),
|
||||
params={
|
||||
"listing_type": listing_type,
|
||||
"min_bedrooms": min_bedrooms,
|
||||
"max_bedrooms": max_bedrooms,
|
||||
"min_price": min_price,
|
||||
"max_price": max_price,
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
else:
|
||||
repository = ListingRepository(engine=engine)
|
||||
result = asyncio.run(
|
||||
listing_service.refresh_listings(
|
||||
repository, qp, async_mode=False,
|
||||
)
|
||||
)
|
||||
data = {"message": result.message, "new_count": result.new_listings_count}
|
||||
|
||||
output(data, cli_ctx.json_output)
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_output(f"HTTP {e.response.status_code}: {e.response.text}", cli_ctx.json_output)
|
||||
except Exception as e:
|
||||
error_output(str(e), cli_ctx.json_output)
|
||||
2
main.py
2
main.py
|
|
@ -475,6 +475,7 @@ from cli.districts import districts_group
|
|||
from cli.decisions import decisions_group
|
||||
from cli.pois import pois_group
|
||||
from cli.tasks import tasks_group
|
||||
from cli.listings import listings_group
|
||||
|
||||
|
||||
@cli.group("debug")
|
||||
|
|
@ -498,6 +499,7 @@ debug.add_command(districts_group)
|
|||
debug.add_command(decisions_group)
|
||||
debug.add_command(pois_group)
|
||||
debug.add_command(tasks_group)
|
||||
debug.add_command(listings_group)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue