233 lines
8.5 KiB
Python
233 lines
8.5 KiB
Python
"""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)
|