From cb7d9212e0d09ed882677c651638b6f39e6672a2 Mon Sep 17 00:00:00 2001 From: tech Date: Tue, 21 Apr 2026 15:57:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20backup=20command=20=E2=80=94=20filter?= =?UTF-8?q?=20scan=20CSV=20by=20state=20and=20date=20range?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/order_bak/backup.py | 44 +++++++++++++++++++++++++++++++ src/order_bak/cli.py | 30 +++++++++++++++++++++ tests/test_backup.py | 58 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 src/order_bak/backup.py create mode 100644 tests/test_backup.py diff --git a/src/order_bak/backup.py b/src/order_bak/backup.py new file mode 100644 index 0000000..470ec2e --- /dev/null +++ b/src/order_bak/backup.py @@ -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 diff --git a/src/order_bak/cli.py b/src/order_bak/cli.py index ff99143..af3745c 100644 --- a/src/order_bak/cli.py +++ b/src/order_bak/cli.py @@ -1,3 +1,4 @@ +from datetime import timezone from pathlib import Path 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__": main() diff --git a/tests/test_backup.py b/tests/test_backup.py new file mode 100644 index 0000000..0ddcdf9 --- /dev/null +++ b/tests/test_backup.py @@ -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"