docs: add implementation plan for backup --days parameter
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
282
docs/superpowers/plans/2026-04-21-backup-days-param.md
Normal file
282
docs/superpowers/plans/2026-04-21-backup-days-param.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# Backup --days Parameter 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:** Replace `--start`/`--end` date range params on the `backup` command with a single `--days N` param that backs up all orders older than N days.
|
||||
|
||||
**Architecture:** The `backup_orders` function currently takes `start`/`end` datetime pair and filters `[start, end)`. We change it to accept a single `cutoff` datetime and filter `OrderTime < cutoff`. The CLI computes `cutoff = today_00_utc - timedelta(days=N)`.
|
||||
|
||||
**Tech Stack:** Python, Click, pytest
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Update `backup.py` — change function signature and filter logic
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/order_bak/backup.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
In `tests/test_backup.py`, add a new test that uses the `cutoff` signature:
|
||||
|
||||
```python
|
||||
def test_backup_filters_by_cutoff(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, 2, 8, tzinfo=timezone.utc), 1),
|
||||
_make_entity("u3", "o3", datetime(2025, 1, 3, 8, tzinfo=timezone.utc), 1), # at/after cutoff
|
||||
]
|
||||
scan_csv = scan_dir / "scan-2025-01-03.csv"
|
||||
write_csv(scan_csv, entities)
|
||||
|
||||
cutoff = datetime(2025, 1, 3, tzinfo=timezone.utc)
|
||||
result = backup_orders(
|
||||
scan_path=scan_csv,
|
||||
backup_dir=backup_dir,
|
||||
cutoff=cutoff,
|
||||
)
|
||||
|
||||
assert result == 2
|
||||
assert (backup_dir / "2025-01-01.csv").exists()
|
||||
assert (backup_dir / "2025-01-02.csv").exists()
|
||||
assert not (backup_dir / "2025-01-03.csv").exists()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_backup.py::test_backup_filters_by_cutoff -v`
|
||||
Expected: FAIL — `backup_orders()` got unexpected keyword `cutoff`
|
||||
|
||||
- [ ] **Step 3: Update `backup_orders` signature and filter**
|
||||
|
||||
In `src/order_bak/backup.py`, replace the function:
|
||||
|
||||
```python
|
||||
def backup_orders(
|
||||
scan_path: Path,
|
||||
backup_dir: Path,
|
||||
cutoff: 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 >= cutoff:
|
||||
continue
|
||||
date_key = order_time.strftime("%Y-%m-%d")
|
||||
by_date[date_key].append(entity)
|
||||
|
||||
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(entity_to_row(r) for r in 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::test_backup_filters_by_cutoff -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Update the old test to use `cutoff`**
|
||||
|
||||
In `tests/test_backup.py`, replace `test_backup_filters_by_state_and_date`:
|
||||
|
||||
```python
|
||||
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), # at cutoff, skip
|
||||
]
|
||||
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,
|
||||
cutoff=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 6: Run all backup tests**
|
||||
|
||||
Run: `uv run pytest tests/test_backup.py -v`
|
||||
Expected: All PASS
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/order_bak/backup.py tests/test_backup.py
|
||||
git commit -m "refactor: change backup_orders to use cutoff param instead of start/end"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update `cli.py` — replace `--start`/`--end` with `--days`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/order_bak/cli.py`
|
||||
|
||||
- [ ] **Step 1: Update the backup command in `cli.py`**
|
||||
|
||||
Replace the `backup` command function in `src/order_bak/cli.py`:
|
||||
|
||||
```python
|
||||
@main.command()
|
||||
@click.option("--days", required=True, type=int, help="Backup orders older than N days (min 1)")
|
||||
@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, days, scan_path):
|
||||
if days < 1:
|
||||
click.echo("Error: --days must be at least 1")
|
||||
raise SystemExit(1)
|
||||
|
||||
cfg = load_config(ctx.obj["config_path"])
|
||||
from order_bak.backup import backup_orders
|
||||
|
||||
if scan_path:
|
||||
scan_file = Path(scan_path)
|
||||
else:
|
||||
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]
|
||||
|
||||
from datetime import timedelta
|
||||
today_utc = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
cutoff = today_utc - timedelta(days=days)
|
||||
|
||||
click.echo(f"Backing up orders before {cutoff.strftime('%Y-%m-%d')} UTC")
|
||||
backup_orders(
|
||||
scan_path=scan_file,
|
||||
backup_dir=Path("./backups"),
|
||||
cutoff=cutoff,
|
||||
)
|
||||
```
|
||||
|
||||
Add `from datetime import datetime, timedelta, timezone` to the top-level imports (replace the existing `from datetime import timezone`).
|
||||
|
||||
- [ ] **Step 2: Run all tests**
|
||||
|
||||
Run: `uv run pytest tests/ -v`
|
||||
Expected: All PASS (backup tests already updated in Task 1)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/order_bak/cli.py
|
||||
git commit -m "feat: replace --start/--end with --days param on backup command"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add edge case tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/test_backup.py`
|
||||
|
||||
- [ ] **Step 1: Add edge case tests**
|
||||
|
||||
Append to `tests/test_backup.py`:
|
||||
|
||||
```python
|
||||
def test_backup_cutoff_at_midnight(tmp_path):
|
||||
"""Order exactly at cutoff midnight should be excluded."""
|
||||
scan_dir = tmp_path / "scan"
|
||||
backup_dir = tmp_path / "backups"
|
||||
scan_dir.mkdir()
|
||||
backup_dir.mkdir()
|
||||
|
||||
entities = [
|
||||
_make_entity("u1", "o1", datetime(2025, 1, 3, 0, 0, 0, tzinfo=timezone.utc), 1), # exactly at cutoff
|
||||
_make_entity("u2", "o2", datetime(2025, 1, 2, 23, 59, 59, tzinfo=timezone.utc), 1), # just before
|
||||
]
|
||||
scan_csv = scan_dir / "scan.csv"
|
||||
write_csv(scan_csv, entities)
|
||||
|
||||
result = backup_orders(
|
||||
scan_path=scan_csv,
|
||||
backup_dir=backup_dir,
|
||||
cutoff=datetime(2025, 1, 3, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
assert result == 1
|
||||
assert (backup_dir / "2025-01-02.csv").exists()
|
||||
assert not (backup_dir / "2025-01-03.csv").exists()
|
||||
|
||||
|
||||
def test_backup_empty_result(tmp_path):
|
||||
"""No orders before cutoff returns 0 and no files."""
|
||||
scan_dir = tmp_path / "scan"
|
||||
backup_dir = tmp_path / "backups"
|
||||
scan_dir.mkdir()
|
||||
backup_dir.mkdir()
|
||||
|
||||
entities = [
|
||||
_make_entity("u1", "o1", datetime(2025, 1, 5, 10, tzinfo=timezone.utc), 1),
|
||||
]
|
||||
scan_csv = scan_dir / "scan.csv"
|
||||
write_csv(scan_csv, entities)
|
||||
|
||||
result = backup_orders(
|
||||
scan_path=scan_csv,
|
||||
backup_dir=backup_dir,
|
||||
cutoff=datetime(2025, 1, 1, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
assert result == 0
|
||||
assert list(backup_dir.glob("*.csv")) == []
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run all tests**
|
||||
|
||||
Run: `uv run pytest tests/test_backup.py -v`
|
||||
Expected: All 4 tests PASS
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/test_backup.py
|
||||
git commit -m "test: add edge cases for backup cutoff filtering"
|
||||
```
|
||||
Reference in New Issue
Block a user