init commit
This commit is contained in:
422
docs/superpowers/plans/2026-04-08-order-query.md
Normal file
422
docs/superpowers/plans/2026-04-08-order-query.md
Normal file
@@ -0,0 +1,422 @@
|
||||
# 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:
|
||||
|
||||
```toml
|
||||
[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**
|
||||
|
||||
```bash
|
||||
mkdir -p order_query
|
||||
touch order_query/__init__.py
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```python
|
||||
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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```python
|
||||
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**
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```python
|
||||
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**
|
||||
|
||||
```python
|
||||
def main():
|
||||
app = OrderQueryApp()
|
||||
app.run()
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore: verify integration and polish"
|
||||
```
|
||||
Reference in New Issue
Block a user