Files
order-query/docs/superpowers/plans/2026-04-08-order-query.md
2026-04-08 17:18:55 +08:00

12 KiB

Order Query Tool Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a Python CLI tool that queries Azure Table Storage for orders by userId and displays results in a Textual TUI.

Architecture: Single-module app (order_query/main.py) with config loading from ~/.order-query/config.toml, Azure Table Storage queries, and a 3-screen Textual TUI (input → list → detail).

Tech Stack: Python 3.13+, uv, azure-data-tables, textual, tomllib (stdlib)


File Structure

File Responsibility
pyproject.toml Project metadata, dependencies, [project.scripts] entry point
order_query/__init__.py Package marker (empty)
order_query/main.py All logic: config, Azure client, Textual TUI app

Task 1: Scaffold project with uv

  • Step 1: Initialize uv project

Run: cd /Users/tech/workspace/n3-world/handy-tools/order-query && uv init --name order-query --python 3.13

  • Step 2: Update pyproject.toml with dependencies and entry point

Replace the generated pyproject.toml with:

[project]
name = "order-query"
version = "0.1.0"
description = "Query order details from Azure Table Storage"
requires-python = ">=3.11"
dependencies = [
    "azure-data-tables>=12.9.0",
    "textual>=3.0.0",
]

[project.scripts]
order-query = "order_query.main:main"
  • Step 3: Install dependencies

Run: uv sync

Expected: dependencies installed successfully

  • Step 4: Create package directory and init file
mkdir -p order_query
touch order_query/__init__.py
  • Step 5: Commit
git add pyproject.toml uv.lock order_query/__init__.py
git commit -m "chore: scaffold uv project with dependencies"

Task 2: Implement config loading

Files:

  • Modify: order_query/main.py

  • Step 1: Write config module in main.py

import tomllib
from pathlib import Path
from dataclasses import dataclass

CONFIG_DIR = Path.home() / ".order-query"
CONFIG_FILE = CONFIG_DIR / "config.toml"

@dataclass
class Config:
    connection_string: str
    table_name: str = "order"

def load_config() -> Config:
    if not CONFIG_FILE.exists():
        CONFIG_DIR.mkdir(parents=True, exist_ok=True)
        CONFIG_FILE.write_text(
            '# Order Query Tool Configuration\n'
            'connection_string = "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;TableEndpoint=https://..."\n'
            'table_name = "order"\n'
        )
        raise SystemExit(
            f"Config file created at {CONFIG_FILE}\n"
            f"Please edit it with your Azure Storage connection string and run again."
        )
    with open(CONFIG_FILE, "rb") as f:
        data = tomllib.load(f)
    return Config(
        connection_string=data["connection_string"],
        table_name=data.get("table_name", "order"),
    )
  • Step 2: Test config auto-creation by running temporarily

Add a temporary if __name__ == "__main__": block to call load_config() and verify file is created at ~/.order-query/config.toml.

  • Step 3: Commit
git add order_query/main.py
git commit -m "feat: add config loading with auto-creation"

Task 3: Implement Azure Table Storage client

Files:

  • Modify: order_query/main.py

  • Step 1: Add dataclass for Order and query function

from dataclasses import dataclass, field
from datetime import datetime
from azure.data.tables import TableServiceClient

@dataclass
class Order:
    order_id: str
    prod_id: str = ""
    prod_count: int = 0
    prod_price: float = 0.0
    revenue: float = 0.0
    state: int = 0
    order_time: str = ""
    receive_time: str = ""
    platform_order: str = ""
    custom_data: str = ""
    ver: int = 0

    @property
    def state_text(self) -> str:
        return "Payed" if self.state == 0 else "Recived"

def query_orders(config: Config, user_id: str) -> list[Order]:
    client = TableServiceClient.from_connection_string(config.connection_string)
    table = client.get_table_client(config.table_name)
    entities = table.query_entities(query_filter=f"PartitionKey eq '{user_id}'")
    orders = []
    for entity in entities:
        order = Order(
            order_id=entity.get("RowKey", ""),
            prod_id=entity.get("prodId", ""),
            prod_count=entity.get("prodCount", 0) or 0,
            prod_price=entity.get("prodPrice", 0.0) or 0.0,
            revenue=entity.get("revenue", 0.0) or 0.0,
            state=entity.get("state", 0),
            order_time=str(entity.get("OrderTime", "")),
            receive_time=str(entity.get("ReceiveTime", "")),
            platform_order=entity.get("platformOrder", ""),
            custom_data=entity.get("customData", ""),
            ver=entity.get("ver", 0) or 0,
        )
        orders.append(order)
    return orders
  • Step 2: Commit
git add order_query/main.py
git commit -m "feat: add Azure Table Storage query client"

Task 4: Implement Textual TUI

Files:

  • Modify: order_query/main.py

  • Step 1: Implement the 3-screen Textual app

The app has three screens:

  1. InputScreen — text input for userId
  2. OrderListScreen — DataTable showing all orders
  3. OrderDetailScreen — detailed view of a single order
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Input, Button, DataTable, Static
from textual.screen import ModalScreen
from textual.containers import Horizontal, Vertical, VerticalScroll

class InputScreen(ModalScreen[str]):
    """Screen for entering userId."""

    def compose(self) -> ComposeResult:
        yield Vertical(
            Static("Enter User ID:", classes="label"),
            Input(placeholder="userId", id="user_id_input"),
            Horizontal(
                Button("Query", variant="primary", id="query_btn"),
                Button("Quit", variant="default", id="quit_btn"),
                classes="buttons",
            ),
            classes="input-container",
        )

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "query_btn":
            user_id = self.query_one("#user_id_input", Input).value.strip()
            if user_id:
                self.dismiss(user_id)
        elif event.button.id == "quit_btn":
            self.app.exit()


class OrderListScreen(ModalScreen[str | None]):
    """Screen showing order list as a DataTable."""

    def __init__(self, orders: list[Order]) -> None:
        super().__init__()
        self.orders = orders

    def compose(self) -> ComposeResult:
        yield Vertical(
            Static(f"Found {len(self.orders)} orders. Select one for details, or press Q to quit."),
            DataTable(id="order_table"),
            Button("Back", id="back_btn"),
            classes="list-container",
        )

    def on_mount(self) -> None:
        table = self.query_one("#order_table", DataTable)
        table.add_columns(
            "Order ID", "Product ID", "Count", "Price", "Revenue", "State", "Order Time"
        )
        for order in self.orders:
            table.add_row(
                order.order_id,
                order.prod_id,
                str(order.prod_count),
                f"{order.prod_price:.2f}",
                f"{order.revenue:.2f}",
                order.state_text,
                order.order_time[:19] if order.order_time else "",
                key=order.order_id,
            )

    def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
        order_id = event.row_key.value
        self.dismiss(order_id)

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "back_btn":
            self.dismiss(None)


class OrderDetailScreen(ModalScreen[None]):
    """Screen showing order details."""

    def __init__(self, order: Order) -> None:
        super().__init__()
        self.order = order

    def compose(self) -> ComposeResult:
        o = self.order
        detail_text = (
            f"[bold]Order ID:[/bold] {o.order_id}\n"
            f"[bold]Product ID:[/bold] {o.prod_id}\n"
            f"[bold]Product Count:[/bold] {o.prod_count}\n"
            f"[bold]Product Price:[/bold] {o.prod_price:.2f}\n"
            f"[bold]Revenue:[/bold] {o.revenue:.2f}\n"
            f"[bold]State:[/bold] {o.state_text}\n"
            f"[bold]Order Time:[/bold] {o.order_time[:19] if o.order_time else 'N/A'}\n"
            f"[bold]Receive Time:[/bold] {o.receive_time[:19] if o.receive_time else 'N/A'}\n"
            f"[bold]Platform Order:[/bold] {o.platform_order or 'N/A'}\n"
            f"[bold]Version:[/bold] {o.ver}\n"
            f"[bold]Custom Data:[/bold] {o.custom_data or 'N/A'}"
        )
        yield Vertical(
            Static(detail_text, id="detail_text"),
            Button("Back", id="back_btn"),
            classes="detail-container",
        )

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.dismiss(None)


class OrderQueryApp(App):
    """Main TUI application."""

    CSS = """
    .input-container {
        align: center middle;
        width: 60;
        height: auto;
        padding: 2;
        border: round $primary;
    }
    .label {
        text-align: center;
        margin-bottom: 1;
        text-style: bold;
    }
    .buttons {
        margin-top: 1;
        align: center middle;
    }
    .list-container {
        padding: 1;
    }
    .detail-container {
        align: center middle;
        width: 80;
        height: auto;
        padding: 2;
        border: round $primary;
    }
    #detail_text {
        margin-bottom: 1;
    }
    #back_btn {
        margin-top: 1;
    }
    """

    BINDINGS = [("q", "quit", "Quit")]

    def on_mount(self) -> None:
        self.push_screen(InputScreen(), self.handle_user_id)

    def handle_user_id(self, user_id: str | None) -> None:
        if user_id is None:
            return
        try:
            config = load_config()
            orders = query_orders(config, user_id)
        except Exception as e:
            self.notify(str(e), severity="error")
            self.push_screen(InputScreen(), self.handle_user_id)
            return
        if not orders:
            self.notify("No orders found for this user", severity="warning")
            self.push_screen(InputScreen(), self.handle_user_id)
            return
        self.push_screen(OrderListScreen(orders), self.handle_order_selected)

    def handle_order_selected(self, order_id: str | None) -> None:
        if order_id is None:
            self.push_screen(InputScreen(), self.handle_user_id)
            return
        order = next((o for o in self._current_orders if o.order_id == order_id), None)
        if order:
            self.push_screen(OrderDetailScreen(order), lambda _: self.show_list_again())

    def show_list_again(self) -> None:
        self.push_screen(OrderListScreen(self._current_orders), self.handle_order_selected)

    def handle_user_id(self, user_id: str | None) -> None:
        if user_id is None:
            return
        try:
            config = load_config()
            orders = query_orders(config, user_id)
        except Exception as e:
            self.notify(str(e), severity="error")
            self.push_screen(InputScreen(), self.handle_user_id)
            return
        self._current_orders = orders
        if not orders:
            self.notify("No orders found for this user", severity="warning")
            self.push_screen(InputScreen(), self.handle_user_id)
            return
        self.push_screen(OrderListScreen(orders), self.handle_order_selected)
  • Step 2: Add main entry point
def main():
    app = OrderQueryApp()
    app.run()
  • Step 3: Commit
git add order_query/main.py
git commit -m "feat: implement Textual TUI with 3-screen flow"

Task 5: Integration test and polish

  • Step 1: Test the tool runs

Run: uv run python -m order_query.main

Expected: TUI window opens with userId input prompt

  • Step 2: Verify config auto-creation

Delete ~/.order-query/config.toml if exists, run the tool again, confirm it creates the file and exits with instructions.

  • Step 3: Commit
git add -A
git commit -m "chore: verify integration and polish"