Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 87 additions & 26 deletions src/gurkerlcli/commands/search_cmd.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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))
Expand Down
30 changes: 30 additions & 0 deletions src/gurkerlcli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading