26 KiB
Order Backup CLI — 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 CLI tool that scans all orders from Azure Table, backs them up to per-date CSVs by time range, and deletes backed-up records from production.
Architecture: Three-phase pipeline — scan (Azure → local CSV), backup (local CSV → filtered per-date CSVs), delete (CSV → Azure batch delete). Each phase is a separate CLI command. Config via TOML file.
Tech Stack: Python 3.11+, uv, azure-data-tables, click (CLI), tomllib (stdlib)
File Structure
order-bak/
├── pyproject.toml
├── src/
│ └── order_bak/
│ ├── __init__.py
│ ├── cli.py # Click CLI entry point (3 commands)
│ ├── config.py # Load ~/.config/order-bak/config.toml
│ ├── csv_utils.py # CSV column defs, write/read helpers
│ ├── scan.py # Scan command logic
│ ├── backup.py # Backup command logic
│ └── delete.py # Delete command logic
└── tests/
├── __init__.py
├── test_config.py
├── test_csv_utils.py
├── test_backup.py
└── test_delete.py
Task 1: Project Scaffold
Files:
-
Create:
pyproject.toml -
Create:
src/order_bak/__init__.py -
Create:
tests/__init__.py -
Step 1: Create pyproject.toml
[project]
name = "order-bak"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"azure-data-tables>=12.5",
"click>=8.1",
]
[project.scripts]
order-bak = "order_bak.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/order_bak"]
[dependency-groups]
dev = [
"pytest>=8.0",
]
- Step 2: Create package init and test init
src/order_bak/__init__.py — empty file.
tests/__init__.py — empty file.
- Step 3: Install dependencies
Run: uv sync --group dev
Expected: dependencies installed successfully
- Step 4: Verify entry point works
src/order_bak/cli.py:
import click
@click.group()
def main():
pass
if __name__ == "__main__":
main()
Run: uv run order-bak
Expected: no error, shows help prompt
- Step 5: Commit
git add pyproject.toml src/ tests/
git commit -m "feat: project scaffold with uv, click, azure-data-tables"
Task 2: Config Module
Files:
-
Create:
src/order_bak/config.py -
Create:
tests/test_config.py -
Step 1: Write failing test
# tests/test_config.py
import tomllib
from pathlib import Path
from unittest.mock import patch
from order_bak.config import load_config
def test_load_config_defaults(tmp_path):
config_path = tmp_path / "config.toml"
config_path.write_text(tomllib.dumps({
"azure": {"connection_string": "conn_str_val"},
}))
cfg = load_config(config_path)
assert cfg.azure_connection_string == "conn_str_val"
assert cfg.scan_page_size == 500
assert cfg.scan_delay_ms == 200
assert cfg.scan_dir == Path("./scan")
assert cfg.delete_batch_size == 100
assert cfg.delete_delay_ms == 300
def test_load_config_custom_values(tmp_path):
config_path = tmp_path / "config.toml"
config_path.write_text(tomllib.dumps({
"azure": {"connection_string": "conn_str_val"},
"scan": {"page_size": 1000, "delay_ms": 500, "scan_dir": "/data/scan"},
"delete": {"batch_size": 50, "delay_ms": 100},
}))
cfg = load_config(config_path)
assert cfg.scan_page_size == 1000
assert cfg.scan_delay_ms == 500
assert cfg.scan_dir == Path("/data/scan")
assert cfg.delete_batch_size == 50
assert cfg.delete_delay_ms == 100
def test_load_config_missing_azure_raises(tmp_path):
config_path = tmp_path / "config.toml"
config_path.write_text("[scan]\npage_size = 100\n")
try:
load_config(config_path)
assert False, "should have raised"
except SystemExit:
pass
- Step 2: Run test to verify it fails
Run: uv run pytest tests/test_config.py -v
Expected: FAIL — ModuleNotFoundError: No module named 'order_bak.config'
- Step 3: Implement config
# src/order_bak/config.py
import sys
import tomllib
from dataclasses import dataclass
from pathlib import Path
@dataclass
class Config:
azure_connection_string: str
scan_page_size: int = 500
scan_delay_ms: int = 200
scan_dir: Path = Path("./scan")
delete_batch_size: int = 100
delete_delay_ms: int = 300
def load_config(config_path: Path) -> Config:
if not config_path.exists():
print(f"Config file not found: {config_path}")
sys.exit(1)
with open(config_path, "rb") as f:
raw = tomllib.load(f)
azure = raw.get("azure", {})
connection_string = azure.get("connection_string")
if not connection_string:
print("Missing azure.connection_string in config")
sys.exit(1)
scan = raw.get("scan", {})
delete = raw.get("delete", {})
return Config(
azure_connection_string=connection_string,
scan_page_size=scan.get("page_size", 500),
scan_delay_ms=scan.get("delay_ms", 200),
scan_dir=Path(scan.get("scan_dir", "./scan")),
delete_batch_size=delete.get("batch_size", 100),
delete_delay_ms=delete.get("delay_ms", 300),
)
- Step 4: Run test to verify it passes
Run: uv run pytest tests/test_config.py -v
Expected: 3 passed
- Step 5: Commit
git add src/order_bak/config.py tests/test_config.py
git commit -m "feat: config module with TOML loading"
Task 3: CSV Utilities
Files:
-
Create:
src/order_bak/csv_utils.py -
Create:
tests/test_csv_utils.py -
Step 1: Write failing test
# tests/test_csv_utils.py
import csv
from datetime import datetime, timezone
from pathlib import Path
from order_bak.csv_utils import (
CSV_COLUMNS,
entity_to_row,
row_to_entity,
write_csv,
read_csv,
)
def test_csv_columns_defined():
assert "PartitionKey" in CSV_COLUMNS
assert "RowKey" in CSV_COLUMNS
assert "OrderTime" in CSV_COLUMNS
assert "state" in CSV_COLUMNS
assert len(CSV_COLUMNS) == 13
def test_entity_to_row_converts_datetime():
entity = {
"PartitionKey": "user1",
"RowKey": "order1",
"OrderTime": datetime(2025, 6, 15, 10, 30, 0, tzinfo=timezone.utc),
"ReceiveTime": datetime(2025, 6, 15, 11, 0, 0, tzinfo=timezone.utc),
"revenue": 99.5,
"platformOrder": "plat123",
"prodCount": 2,
"pfId": "user1",
"prodId": "prod1",
"prodPrice": 49.75,
"state": 1,
"ver": 3,
"customData": '{"key":"val"}',
}
row = entity_to_row(entity)
assert row["OrderTime"] == "2025-06-15T10:30:00+00:00"
assert row["ReceiveTime"] == "2025-06-15T11:00:00+00:00"
assert row["revenue"] == "99.5"
assert row["state"] == "1"
def test_entity_to_row_handles_none():
entity = {
"PartitionKey": "user1",
"RowKey": "order1",
"OrderTime": datetime(2025, 1, 1, tzinfo=timezone.utc),
"ReceiveTime": None,
"revenue": None,
"platformOrder": None,
"prodCount": None,
"pfId": None,
"prodId": None,
"prodPrice": None,
"state": 0,
"ver": None,
"customData": None,
}
row = entity_to_row(entity)
assert row["ReceiveTime"] == ""
assert row["revenue"] == ""
def test_row_to_entity_roundtrip(tmp_path):
original = {
"PartitionKey": "user1",
"RowKey": "order1",
"OrderTime": "2025-06-15T10:30:00+00:00",
"ReceiveTime": "",
"revenue": "99.5",
"platformOrder": "plat123",
"prodCount": "2",
"pfId": "user1",
"prodId": "prod1",
"prodPrice": "49.75",
"state": "1",
"ver": "3",
"customData": '{"key":"val"}',
}
entity = row_to_entity(original)
assert entity["PartitionKey"] == "user1"
assert entity["state"] == 1
assert entity["revenue"] == 99.5
assert entity["prodCount"] == 2
assert entity["ReceiveTime"] is None
def test_write_and_read_csv(tmp_path):
entities = [
{
"PartitionKey": "user1",
"RowKey": "order1",
"OrderTime": datetime(2025, 6, 15, 10, 30, tzinfo=timezone.utc),
"ReceiveTime": None,
"revenue": 99.5,
"platformOrder": "p1",
"prodCount": 2,
"pfId": "user1",
"prodId": "prod1",
"prodPrice": 49.75,
"state": 1,
"ver": 3,
"customData": None,
},
{
"PartitionKey": "user2",
"RowKey": "order2",
"OrderTime": datetime(2025, 6, 16, 12, 0, tzinfo=timezone.utc),
"ReceiveTime": datetime(2025, 6, 16, 13, 0, tzinfo=timezone.utc),
"revenue": 10.0,
"platformOrder": "p2",
"prodCount": 1,
"pfId": "user2",
"prodId": "prod2",
"prodPrice": 10.0,
"state": 1,
"ver": 3,
"customData": '{"note":"test"}',
},
]
csv_path = tmp_path / "test.csv"
write_csv(csv_path, entities)
result = list(read_csv(csv_path))
assert len(result) == 2
assert result[0]["PartitionKey"] == "user1"
assert result[1]["PartitionKey"] == "user2"
assert result[1]["revenue"] == 10.0
assert result[0]["ReceiveTime"] is None
assert result[1]["ReceiveTime"] is not None
- Step 2: Run test to verify it fails
Run: uv run pytest tests/test_csv_utils.py -v
Expected: FAIL — ModuleNotFoundError: No module named 'order_bak.csv_utils'
- Step 3: Implement csv_utils
# src/order_bak/csv_utils.py
import csv
from datetime import datetime
from pathlib import Path
from typing import Any
CSV_COLUMNS = [
"PartitionKey",
"RowKey",
"OrderTime",
"ReceiveTime",
"revenue",
"platformOrder",
"prodCount",
"pfId",
"prodId",
"prodPrice",
"state",
"ver",
"customData",
]
def _format_value(val: Any) -> str:
if val is None:
return ""
if isinstance(val, datetime):
return val.isoformat()
return str(val)
def entity_to_row(entity: dict[str, Any]) -> dict[str, str]:
return {col: _format_value(entity.get(col)) for col in CSV_COLUMNS}
def _parse_row(raw: dict[str, str]) -> dict[str, Any]:
result: dict[str, Any] = {}
for col in CSV_COLUMNS:
val = raw.get(col, "")
if val == "":
result[col] = None
continue
if col in ("OrderTime", "ReceiveTime"):
result[col] = datetime.fromisoformat(val)
elif col in ("revenue", "prodPrice"):
result[col] = float(val)
elif col in ("prodCount", "state", "ver"):
result[col] = int(val)
else:
result[col] = val
return result
def row_to_entity(raw: dict[str, str]) -> dict[str, Any]:
return _parse_row(raw)
def write_csv(path: Path, entities: list[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS)
writer.writeheader()
for entity in entities:
writer.writerow(entity_to_row(entity))
def read_csv(path: Path):
with open(path, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
yield _parse_row(row)
- Step 4: Run test to verify it passes
Run: uv run pytest tests/test_csv_utils.py -v
Expected: 5 passed
- Step 5: Commit
git add src/order_bak/csv_utils.py tests/test_csv_utils.py
git commit -m "feat: CSV utilities for order entity serialization"
Task 4: Scan Command
Files:
-
Create:
src/order_bak/scan.py -
Modify:
src/order_bak/cli.py -
Step 1: Implement scan logic
# src/order_bak/scan.py
import time
from datetime import datetime, timezone
from pathlib import Path
from azure.data.tables import TableClient
from order_bak.csv_utils import CSV_COLUMNS, write_csv
def scan_orders(
connection_string: str,
page_size: int,
delay_ms: int,
scan_dir: Path,
) -> Path:
table = TableClient.from_connection_string(connection_string, table_name="order")
scan_dir.mkdir(parents=True, exist_ok=True)
now = datetime.now(timezone.utc)
csv_path = scan_dir / f"scan-{now.strftime('%Y-%m-%d')}.csv"
paged = table.list_entities(results_per_page=page_size)
total_scanned = 0
csv_path.parent.mkdir(parents=True, exist_ok=True)
with open(csv_path, "w", newline="") as f:
import csv
writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS)
writer.writeheader()
for page in paged.by_page():
for entity in page:
writer.writerow({col: _fmt(entity.get(col)) for col in CSV_COLUMNS})
total_scanned += 1
if delay_ms > 0:
time.sleep(delay_ms / 1000.0)
print(f"Scan complete: {total_scanned} orders saved to {csv_path}")
return csv_path
def _fmt(val):
if val is None:
return ""
if isinstance(val, datetime):
return val.isoformat()
return str(val)
- Step 2: Wire scan command into CLI
# src/order_bak/cli.py
from pathlib import Path
import click
from order_bak.config import load_config
@click.group()
@click.option("--config", "-c", default="~/.config/order-bak/config.toml", type=click.Path())
@click.pass_context
def main(ctx, config):
ctx.ensure_object(dict)
ctx.obj["config_path"] = Path(config).expanduser()
@main.command()
@click.pass_context
def scan(ctx):
cfg = load_config(ctx.obj["config_path"])
from order_bak.scan import scan_orders
scan_orders(
connection_string=cfg.azure_connection_string,
page_size=cfg.scan_page_size,
delay_ms=cfg.scan_delay_ms,
scan_dir=cfg.scan_dir,
)
if __name__ == "__main__":
main()
- Step 3: Verify CLI help works
Run: uv run order-bak --help
Expected: shows scan command
Run: uv run order-bak scan --help
Expected: shows scan help
- Step 4: Commit
git add src/order_bak/scan.py src/order_bak/cli.py
git commit -m "feat: scan command — full table scan to CSV with throttling"
Task 5: Backup Command
Files:
-
Create:
src/order_bak/backup.py -
Create:
tests/test_backup.py -
Modify:
src/order_bak/cli.py -
Step 1: Write failing test
# tests/test_backup.py
from datetime import datetime, timezone
from pathlib import Path
from order_bak.csv_utils import write_csv
from order_bak.backup import backup_orders
def _make_entity(pk, rk, order_time, state, receive_time=None):
return {
"PartitionKey": pk,
"RowKey": rk,
"OrderTime": order_time,
"ReceiveTime": receive_time,
"revenue": 10.0,
"platformOrder": "plat1",
"prodCount": 1,
"pfId": pk,
"prodId": "prod1",
"prodPrice": 10.0,
"state": state,
"ver": 3,
"customData": None,
}
def test_backup_filters_by_state_and_date(tmp_path):
scan_dir = tmp_path / "scan"
backup_dir = tmp_path / "backups"
scan_dir.mkdir()
backup_dir.mkdir()
entities = [
_make_entity("u1", "o1", datetime(2025, 1, 1, 10, tzinfo=timezone.utc), 1),
_make_entity("u2", "o2", datetime(2025, 1, 1, 12, tzinfo=timezone.utc), 0), # Payed, skip
_make_entity("u3", "o3", datetime(2025, 1, 2, 8, tzinfo=timezone.utc), 1),
_make_entity("u4", "o4", datetime(2025, 1, 3, 8, tzinfo=timezone.utc), 1), # outside range
]
scan_csv = scan_dir / "scan-2025-01-03.csv"
write_csv(scan_csv, entities)
result = backup_orders(
scan_path=scan_csv,
backup_dir=backup_dir,
start=datetime(2025, 1, 1, tzinfo=timezone.utc),
end=datetime(2025, 1, 3, tzinfo=timezone.utc),
)
assert result == 2
jan1 = backup_dir / "2025-01-01.csv"
jan2 = backup_dir / "2025-01-02.csv"
assert jan1.exists()
assert jan2.exists()
assert not (backup_dir / "2025-01-03.csv").exists()
from order_bak.csv_utils import read_csv
jan1_rows = list(read_csv(jan1))
assert len(jan1_rows) == 1
assert jan1_rows[0]["RowKey"] == "o1"
- Step 2: Run test to verify it fails
Run: uv run pytest tests/test_backup.py -v
Expected: FAIL — ModuleNotFoundError: No module named 'order_bak.backup'
- Step 3: Implement backup
# src/order_bak/backup.py
import csv
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from typing import Iterator
from order_bak.csv_utils import CSV_COLUMNS, _parse_row
def backup_orders(
scan_path: Path,
backup_dir: Path,
start: datetime,
end: datetime,
) -> int:
backup_dir.mkdir(parents=True, exist_ok=True)
by_date: dict[str, list[dict]] = defaultdict(list)
with open(scan_path, newline="") as f:
reader = csv.DictReader(f)
for raw_row in reader:
entity = _parse_row(raw_row)
if entity["state"] != 1:
continue
order_time = entity["OrderTime"]
if order_time is None:
continue
if order_time < start or order_time >= end:
continue
date_key = order_time.strftime("%Y-%m-%d")
by_date[date_key].append(raw_row)
total = 0
for date_key, rows in sorted(by_date.items()):
csv_path = backup_dir / f"{date_key}.csv"
with open(csv_path, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS)
writer.writeheader()
writer.writerows(rows)
total += len(rows)
print(f" {date_key}: {len(rows)} orders")
print(f"Backup complete: {total} orders across {len(by_date)} days")
return total
- Step 4: Run test to verify it passes
Run: uv run pytest tests/test_backup.py -v
Expected: 1 passed
- Step 5: Wire backup command into CLI
Add to src/order_bak/cli.py:
@main.command()
@click.option("--start", required=True, type=click.DateTime(formats=["%Y-%m-%d"]), help="Start date (UTC, inclusive)")
@click.option("--end", required=True, type=click.DateTime(formats=["%Y-%m-%d"]), help="End date (UTC, exclusive)")
@click.option("--scan", "scan_path", default=None, type=click.Path(), help="Path to scan CSV (default: latest in scan_dir)")
@click.pass_context
def backup(ctx, start, end, scan_path):
cfg = load_config(ctx.obj["config_path"])
from order_bak.backup import backup_orders
import pathlib
if scan_path:
scan_file = pathlib.Path(scan_path)
else:
import glob
scan_files = sorted(cfg.scan_dir.glob("scan-*.csv"))
if not scan_files:
click.echo("No scan files found in scan_dir")
raise SystemExit(1)
scan_file = scan_files[-1]
start_utc = start.replace(tzinfo=__import__("datetime").timezone.utc)
end_utc = end.replace(tzinfo=__import__("datetime").timezone.utc)
backup_orders(
scan_path=scan_file,
backup_dir=pathlib.Path("./backups"),
start=start_utc,
end=end_utc,
)
- Step 6: Verify CLI help
Run: uv run order-bak backup --help
Expected: shows --start, --end, --scan options
- Step 7: Commit
git add src/order_bak/backup.py tests/test_backup.py src/order_bak/cli.py
git commit -m "feat: backup command — filter scan CSV by state and date range"
Task 6: Delete Command
Files:
-
Create:
src/order_bak/delete.py -
Create:
tests/test_delete.py -
Modify:
src/order_bak/cli.py -
Step 1: Write failing test
# tests/test_delete.py
from collections import defaultdict
from unittest.mock import MagicMock, call
from order_bak.delete import delete_orders
def test_delete_groups_by_partition_and_batches(tmp_path):
# Create a CSV with entities across different partitions
from order_bak.csv_utils import write_csv
from datetime import datetime, timezone
entities = []
# 3 entities in partition "user1", 2 in "user2"
for i in range(3):
entities.append({
"PartitionKey": "user1",
"RowKey": f"order{i}",
"OrderTime": datetime(2025, 1, 1, tzinfo=timezone.utc),
"ReceiveTime": None, "revenue": 10.0, "platformOrder": "p",
"prodCount": 1, "pfId": "user1", "prodId": "prod",
"prodPrice": 10.0, "state": 1, "ver": 3, "customData": None,
})
for i in range(3, 5):
entities.append({
"PartitionKey": "user2",
"RowKey": f"order{i}",
"OrderTime": datetime(2025, 1, 1, tzinfo=timezone.utc),
"ReceiveTime": None, "revenue": 10.0, "platformOrder": "p",
"prodCount": 1, "pfId": "user2", "prodId": "prod",
"prodPrice": 10.0, "state": 1, "ver": 3, "customData": None,
})
csv_path = tmp_path / "2025-01-01.csv"
write_csv(csv_path, entities)
mock_table = MagicMock()
deleted_count = delete_orders(
table_client=mock_table,
csv_path=csv_path,
batch_size=2,
delay_ms=0,
)
assert deleted_count == 5
# user1 has 3 entities, batch_size=2 → 2 batches (2 + 1)
# user2 has 2 entities, batch_size=2 → 1 batch
assert mock_table.submit_transaction.call_count == 3
def test_delete_respects_batch_size(tmp_path):
from order_bak.csv_utils import write_csv
from datetime import datetime, timezone
# 5 entities in same partition, batch_size=2
entities = [
{
"PartitionKey": "user1",
"RowKey": f"order{i}",
"OrderTime": datetime(2025, 1, 1, tzinfo=timezone.utc),
"ReceiveTime": None, "revenue": 10.0, "platformOrder": "p",
"prodCount": 1, "pfId": "user1", "prodId": "prod",
"prodPrice": 10.0, "state": 1, "ver": 3, "customData": None,
}
for i in range(5)
]
csv_path = tmp_path / "2025-01-01.csv"
write_csv(csv_path, entities)
mock_table = MagicMock()
deleted_count = delete_orders(
table_client=mock_table,
csv_path=csv_path,
batch_size=2,
delay_ms=0,
)
assert deleted_count == 5
assert mock_table.submit_transaction.call_count == 3 # 2+2+1
- Step 2: Run test to verify it fails
Run: uv run pytest tests/test_delete.py -v
Expected: FAIL — ModuleNotFoundError: No module named 'order_bak.delete'
- Step 3: Implement delete
# src/order_bak/delete.py
import csv
import time
from collections import defaultdict
from itertools import islice
from pathlib import Path
def _chunked(iterable, size):
it = iter(iterable)
while chunk := list(islice(it, size)):
yield chunk
def delete_orders(
table_client,
csv_path: Path,
batch_size: int,
delay_ms: int,
) -> int:
by_partition: dict[str, list[dict]] = defaultdict(list)
with open(csv_path, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
pk = row["PartitionKey"]
rk = row["RowKey"]
by_partition[pk].append({"PartitionKey": pk, "RowKey": rk})
total_deleted = 0
for pk, entities in by_partition.items():
for batch in _chunked(entities, batch_size):
operations = [("delete", e) for e in batch]
table_client.submit_transaction(operations)
total_deleted += len(batch)
if delay_ms > 0:
time.sleep(delay_ms / 1000.0)
print(f"Deleted {total_deleted} orders across {len(by_partition)} partitions")
return total_deleted
- Step 4: Run test to verify it passes
Run: uv run pytest tests/test_delete.py -v
Expected: 2 passed
- Step 5: Wire delete command into CLI
Add to src/order_bak/cli.py:
@main.command()
@click.option("--csv", "csv_path", required=True, type=click.Path(exists=True), help="Path to backup CSV to delete")
@click.pass_context
def delete(ctx, csv_path):
cfg = load_config(ctx.obj["config_path"])
from azure.data.tables import TableClient
from order_bak.delete import delete_orders
import pathlib
csv_file = pathlib.Path(csv_path)
# Count records first
import csv as csv_mod
with open(csv_file, newline="") as f:
count = sum(1 for _ in csv_mod.DictReader(f))
click.echo(f"Will delete {count} orders from {csv_file}")
if not click.confirm("Continue?"):
click.echo("Aborted")
return
table = TableClient.from_connection_string(cfg.azure_connection_string, table_name="order")
delete_orders(
table_client=table,
csv_path=csv_file,
batch_size=cfg.delete_batch_size,
delay_ms=cfg.delete_delay_ms,
)
- Step 6: Verify CLI help
Run: uv run order-bak delete --help
Expected: shows --csv option
- Step 7: Commit
git add src/order_bak/delete.py tests/test_delete.py src/order_bak/cli.py
git commit -m "feat: delete command — batch delete from CSV with confirmation"
Task 7: Run All Tests
- Step 1: Run full test suite
Run: uv run pytest tests/ -v
Expected: all tests pass
- Step 2: Run lint check
Run: uv run python -m py_compile src/order_bak/cli.py
Expected: no errors
Self-Review Checklist
- Spec coverage: scan (Task 4), backup (Task 5), delete (Task 6), config (Task 2), CSV format (Task 3) — all covered
- No placeholders: all code blocks contain complete implementations
- Type consistency: entity dict keys match across csv_utils, backup, delete — all use
CSV_COLUMNS