diff --git a/.gitignore b/.gitignore index 4f168b2..cb50d91 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ backups/ scan/ dist-source/ .worktrees/ +scripts/azurite/ diff --git a/CLAUDE.md b/CLAUDE.md index 5d0ccd9..fdec8bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,10 @@ uv run pytest tests/ -v -m "not integration" # Run a single test file uv run pytest tests/test_backup.py -v -# Run integration tests (requires Azure Storage emulator at 192.168.9.101) +# Start Azure Storage emulator (required for integration tests) +docker compose -f scripts/docker-compose.yml up -d + +# Run integration tests (requires emulator running via docker compose above) uv run pytest tests/test_integration.py -v # Seed test data into the emulator @@ -72,4 +75,4 @@ Override config path with `order-bak --config /path/to/config.toml `. ### Integration Tests -The integration test (`tests/test_integration.py`) hits a real Azure Storage emulator at `192.168.9.101:10002`. Override the endpoint via `ORDER_BAK_TEST_CONNECTION_STRING` env var. Tests are gated by the `integration` pytest marker. +Integration tests (`tests/test_integration.py`) require the Azure Storage emulator running locally via `scripts/docker-compose.yml` (azurite on `localhost:10002`). Start it with `docker compose -f scripts/docker-compose.yml up -d` before running. Tests are gated by the `integration` pytest marker. Override the endpoint via `ORDER_BAK_TEST_CONNECTION_STRING` env var if needed. diff --git a/assets/config.toml b/assets/config.toml index e60fb8c..7b52c4c 100644 --- a/assets/config.toml +++ b/assets/config.toml @@ -9,6 +9,9 @@ connection_string = "" # delay_ms = 200 # scan_dir = "./scan" +[backup] +# retain_days = 1 + [delete] # batch_size = 100 # delay_ms = 300 diff --git a/assets/local.toml b/assets/local.toml index 6bd6db5..29a6634 100644 --- a/assets/local.toml +++ b/assets/local.toml @@ -7,6 +7,9 @@ page_size = 500 delay_ms = 200 scan_dir = "./scan" +[backup] +retain_days = 1 + [delete] batch_size = 100 delay_ms = 300 diff --git a/assets/s101.toml b/assets/s101.toml index d2f9a09..668c96b 100644 --- a/assets/s101.toml +++ b/assets/s101.toml @@ -7,6 +7,9 @@ page_size = 500 delay_ms = 200 scan_dir = "./scan" +[backup] +retain_days = 1 + [delete] batch_size = 100 delay_ms = 300 diff --git a/assets/sandbox.toml b/assets/sandbox.toml index 3e2350a..8c54015 100644 --- a/assets/sandbox.toml +++ b/assets/sandbox.toml @@ -7,6 +7,9 @@ page_size = 500 delay_ms = 200 scan_dir = "./scan" +[backup] +retain_days = 1 + [delete] batch_size = 100 delay_ms = 300 diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml new file mode 100644 index 0000000..f2d5a0a --- /dev/null +++ b/scripts/docker-compose.yml @@ -0,0 +1,9 @@ +services: + azure: + image: mcr.microsoft.com/azure-storage/azurite + ports: + - 10000:10000 + - 10001:10001 + - 10002:10002 + volumes: + - ./azurite:/data diff --git a/src/order_bak/cli.py b/src/order_bak/cli.py index 658ff57..e68c506 100644 --- a/src/order_bak/cli.py +++ b/src/order_bak/cli.py @@ -31,7 +31,7 @@ def scan(ctx, count): @main.command() -@click.option("--days", required=True, type=int, help="Backup orders older than N days (min 1)") +@click.option("--days", default=None, type=int, help="Backup orders older than N days (min 1); default from config backup.retain_days") @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): @@ -42,6 +42,9 @@ def backup(ctx, days, scan_path): cfg = load_config(ctx.obj["config_path"]) from order_bak.backup import backup_orders + if days is None: + days = cfg.backup_retain_days + if scan_path: scan_file = Path(scan_path) else: diff --git a/src/order_bak/config.py b/src/order_bak/config.py index 3a71225..eec0cd5 100644 --- a/src/order_bak/config.py +++ b/src/order_bak/config.py @@ -12,6 +12,7 @@ class Config: scan_dir: Path = Path("./scan") delete_batch_size: int = 100 delete_delay_ms: int = 300 + backup_retain_days: int = 1 def load_config(config_path: Path) -> Config: @@ -30,6 +31,7 @@ def load_config(config_path: Path) -> Config: scan = raw.get("scan", {}) delete = raw.get("delete", {}) + backup = raw.get("backup", {}) return Config( azure_connection_string=connection_string, @@ -38,4 +40,5 @@ def load_config(config_path: Path) -> Config: scan_dir=Path(scan.get("scan_dir", "./scan")), delete_batch_size=delete.get("batch_size", 100), delete_delay_ms=delete.get("delay_ms", 300), + backup_retain_days=backup.get("retain_days", 1), ) diff --git a/tests/test_integration.py b/tests/test_integration.py index 1b2d014..c2a0a8f 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -14,7 +14,7 @@ from order_bak.csv_utils import read_csv from order_bak.delete import delete_orders from order_bak.scan import scan_orders -CONFIG_PATH = Path(__file__).parent.parent / "assets" / "s101.toml" +CONFIG_PATH = Path(__file__).parent.parent / "assets" / "local.toml" CONNECTION_STRING = os.environ.get( "ORDER_BAK_TEST_CONNECTION_STRING", @@ -23,9 +23,9 @@ CONNECTION_STRING = os.environ.get( "AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/" "K1SZFPTOtr/KBHBeksoGMGw==;" "DefaultEndpointsProtocol=http;" - "BlobEndpoint=http://192.168.9.101:10000/devstoreaccount1;" - "QueueEndpoint=http://192.168.9.101:10001/devstoreaccount1;" - "TableEndpoint=http://192.168.9.101:10002/devstoreaccount1;" + "BlobEndpoint=http://localhost:10000/devstoreaccount1;" + "QueueEndpoint=http://localhost:10001/devstoreaccount1;" + "TableEndpoint=http://localhost:10002/devstoreaccount1;" ), )