diff --git a/src/gurkerlcli/commands/search_cmd.py b/src/gurkerlcli/commands/search_cmd.py index 7834483..2361890 100644 --- a/src/gurkerlcli/commands/search_cmd.py +++ b/src/gurkerlcli/commands/search_cmd.py @@ -1,14 +1,16 @@ """Product search commands.""" import json +from decimal import Decimal import click +from rich.table import Table from ..client import GurkerlClient from ..config import Config from ..exceptions import GurkerlError -from ..models import Product -from ..utils.formatting import format_product_table, print_error, console +from ..models import SearchResult +from ..utils.formatting import print_error, console def _create_search_client(debug: bool = False) -> GurkerlClient: @@ -43,6 +45,30 @@ def _create_search_client(debug: bool = False) -> GurkerlClient: return GurkerlClient(session=filtered_session, debug=debug) +def _format_search_table(results: list[SearchResult]) -> Table: + """Format search results as a table.""" + table = Table(show_header=True, header_style="bold magenta") + table.add_column("ID", style="dim") + table.add_column("Name") + table.add_column("Brand", style="cyan") + table.add_column("Amount") + table.add_column("Price", justify="right", style="green") + table.add_column("Per Unit", justify="right", style="dim") + + for result in results: + per_unit = f"€ {result.price_per_unit:.2f}/{result.unit}" if result.price_per_unit else "" + table.add_row( + str(result.id), + result.name, + result.brand or "", + result.textual_amount, + result.price_display, + per_unit, + ) + + return table + + @click.command(name="search") @click.argument("query") @click.option("--limit", default=20, help="Maximum number of results") @@ -60,35 +86,46 @@ def search(query: str, limit: int, output_json: bool, debug: bool) -> None: with _create_search_client(debug=debug) as client: # Search via autocomplete endpoint response = client.get( - "/services/frontend-service/autocomplete-suggestion", - params={"q": query}, + "/services/frontend-service/autocomplete", + params={ + "search": query, + "referer": "whisperer", + "companyId": "1", + }, ) - # Get product IDs from autocomplete + # Get product IDs from autocomplete response product_ids = [] if "productIds" in response: - # Direct product IDs list product_ids = [str(pid) for pid in response["productIds"][:limit]] - elif "products" in response: - product_ids = [str(p.get("id")) for p in response["products"][:limit]] - elif "data" in response and "products" in response["data"]: - product_ids = [ - str(p.get("id")) for p in response["data"]["products"][:limit] - ] if not product_ids: print_error(f"No products found for '{query}'") return + # Build params for products endpoint (multiple products params) + products_params = [("products", pid) for pid in product_ids] + # Get full product details products_response = client.get( - "/api/v1/products/card", - params={ - "products": product_ids, - "categoryType": "normal", - }, + "/api/v1/products", + params=products_params, + ) + + # Get prices for products + prices_response = client.get( + "/api/v1/products/prices", + params=products_params, ) + # Build price lookup by product ID + prices_by_id = {} + if isinstance(prices_response, list): + for price_item in prices_response: + pid = price_item.get("productId") + if pid: + prices_by_id[pid] = price_item + # Parse product list response if isinstance(products_response, list): products_data = products_response @@ -101,33 +138,57 @@ def search(query: str, limit: int, output_json: bool, debug: bool) -> None: print_error(f"No product details found for '{query}'") return - # Convert to Product models - products = [] + # Convert to SearchResult models + results = [] for item in products_data[:limit]: try: - product = Product(**item) - products.append(product) + pid = item.get("id") + price_data = prices_by_id.get(pid, {}) + + # Extract price info + price = None + price_per_unit = None + currency = "EUR" + + if "price" in price_data: + price = Decimal(str(price_data["price"]["amount"])) + currency = price_data["price"].get("currency", "EUR") + if "pricePerUnit" in price_data: + price_per_unit = Decimal(str(price_data["pricePerUnit"]["amount"])) + + result = SearchResult( + id=pid, + name=item.get("name", ""), + slug=item.get("slug", ""), + brand=item.get("brand"), + unit=item.get("unit", ""), + textualAmount=item.get("textualAmount", ""), + images=item.get("images", []), + price=price, + price_per_unit=price_per_unit, + currency=currency, + ) + results.append(result) except Exception as e: if debug: print_error(f"Failed to parse product: {e}") import traceback - traceback.print_exc() continue - if not products: + if not results: print_error(f"No valid products found for '{query}'") return # Output if output_json: click.echo( - json.dumps([p.model_dump(mode="json") for p in products], indent=2) + json.dumps([r.model_dump(mode="json") for r in results], indent=2) ) else: - table = format_product_table(products) + table = _format_search_table(results) console.print(table) - console.print(f"\n[dim]Found {len(products)} products[/dim]") + console.print(f"\n[dim]Found {len(results)} products[/dim]") except GurkerlError as e: print_error(str(e)) diff --git a/src/gurkerlcli/models.py b/src/gurkerlcli/models.py index f52821c..232c001 100644 --- a/src/gurkerlcli/models.py +++ b/src/gurkerlcli/models.py @@ -8,6 +8,36 @@ from pydantic import BaseModel, Field +class SearchResult(BaseModel): + """Product search result (from /api/v1/products + /api/v1/products/prices).""" + + id: int + name: str + slug: str + brand: str | None = None + unit: str + textual_amount: str = Field(alias="textualAmount") + images: list[str] = Field(default_factory=list) + price: Decimal | None = None + price_per_unit: Decimal | None = None + currency: str = "EUR" + + class Config: + populate_by_name = True + + @property + def image_url(self) -> str: + """Get first image URL.""" + return self.images[0] if self.images else "" + + @property + def price_display(self) -> str: + """Get formatted price.""" + if self.price is not None: + return f"€ {self.price:.2f}" + return "N/A" + + class ProductImage(BaseModel): """Product image.""" diff --git a/uv.lock b/uv.lock index 2754d29..0b19e57 100644 --- a/uv.lock +++ b/uv.lock @@ -137,7 +137,7 @@ wheels = [ [[package]] name = "gurkerlcli" -version = "0.1.0" +version = "0.1.2" source = { editable = "." } dependencies = [ { name = "click" },