feat: backup command — filter scan CSV by state and date range
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
44
src/order_bak/backup.py
Normal file
44
src/order_bak/backup.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import csv
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from datetime import timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -27,5 +28,34 @@ def scan(ctx):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
start_utc = start.replace(tzinfo=timezone.utc)
|
||||||
|
end_utc = end.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
backup_orders(
|
||||||
|
scan_path=scan_file,
|
||||||
|
backup_dir=Path("./backups"),
|
||||||
|
start=start_utc,
|
||||||
|
end=end_utc,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
58
tests/test_backup.py
Normal file
58
tests/test_backup.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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"
|
||||||
Reference in New Issue
Block a user