"""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)