Skip to content

5 cli restore#9

Open
ai-mindset wants to merge 12 commits into
mainfrom
5-cli-restore
Open

5 cli restore#9
ai-mindset wants to merge 12 commits into
mainfrom
5-cli-restore

Conversation

@ai-mindset

@ai-mindset ai-mindset commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Closes #5

Introduces an interactive backup and restore CLI, along with improvements to the backup/restore core logic. The changes enhance backup validation, snapshot pruning, type-safe serialization, and restore fidelity. The CLI allows users to select source/target tables and specific snapshots, and the backup system now supports tagged EDM types for reliable round-tripping. Additionally, the Azure Functions app gains a manual dev backup endpoint, and developer tooling is improved.

CLI test coverage has been added in tests/test_cli.py (25 tests) covering _confirm, _find_snapshots, _resolve_snapshot, _download_snapshot, and main() — including the confirm/abort flow, snapshot resolution paths, and safe filename generation.

$  uv run pytest
========================================================================================================================== test session starts ===========================================================================================================================
platform linux -- Python 3.13.0, pytest-9.0.3, pluggy-1.6.0 -- ~/git/nhp_ats_backup/.venv/bin/python
cachedir: .pytest_cache
rootdir: ~/git/nhp_ats_backup
configfile: pyproject.toml
collected 36 items

tests/test_cli.py::test_confirm_returns_true_for_affirmative[y] PASSED
tests/test_cli.py::test_confirm_returns_true_for_affirmative[yes] PASSED
tests/test_cli.py::test_confirm_returns_true_for_affirmative[Y] PASSED
tests/test_cli.py::test_confirm_returns_true_for_affirmative[YES] PASSED
tests/test_cli.py::test_confirm_returns_true_for_affirmative[Yes] PASSED
tests/test_cli.py::test_confirm_returns_false_for_non_affirmative[n] PASSED
tests/test_cli.py::test_confirm_returns_false_for_non_affirmative[no] PASSED
tests/test_cli.py::test_confirm_returns_false_for_non_affirmative[N] PASSED
tests/test_cli.py::test_confirm_returns_false_for_non_affirmative[] PASSED
tests/test_cli.py::test_confirm_returns_false_for_non_affirmative[maybe] PASSED
tests/test_cli.py::test_confirm_returns_false_for_non_affirmative[nope] PASSED
tests/test_cli.py::test_find_snapshots_excludes_status_json PASSED
tests/test_cli.py::test_find_snapshots_filters_by_prefix PASSED
tests/test_cli.py::test_find_snapshots_returns_sorted PASSED
tests/test_cli.py::test_find_snapshots_returns_empty_for_no_matches PASSED
tests/test_cli.py::test_resolve_snapshot_reads_status_json_when_no_date PASSED
tests/test_cli.py::test_resolve_snapshot_raises_when_status_json_missing_field PASSED
tests/test_cli.py::test_resolve_snapshot_picks_latest_for_date_prefix PASSED
tests/test_cli.py::test_resolve_snapshot_raises_when_no_match_for_date PASSED
tests/test_cli.py::test_download_snapshot_writes_file_and_returns_safe_path PASSED
tests/test_cli.py::test_main_backup_and_restore_skipped_on_no PASSED
tests/test_cli.py::test_main_returns_130_on_keyboard_interrupt PASSED
tests/test_cli.py::test_main_backup_runs_when_confirmed PASSED
tests/test_cli.py::test_main_restore_runs_when_confirmed PASSED
tests/test_cli.py::test_main_returns_1_when_snapshot_fetch_fails PASSED
tests/test_core.py::test_fetch_entities_returns_tagged_dicts PASSED
tests/test_core.py::test_fetch_entities_empty_table PASSED
tests/test_core.py::test_prune_deletes_oldest_when_over_limit PASSED
tests/test_core.py::test_prune_does_not_delete_when_at_limit PASSED
tests/test_core.py::test_prune_excludes_status_blob PASSED
tests/test_core.py::test_run_backup_uploads_snapshot_and_status PASSED
tests/test_core.py::test_run_restore_upserts_all_entities PASSED
tests/test_core.py::test_run_restore_creates_table_if_missing PASSED
tests/test_core.py::test_run_restore_handles_tagged_entities_with_datetime PASSED
tests/test_core.py::test_missing_env_var_raises PASSED
tests/test_core.py::test_all_types_roundtrip PASSED

=========================================================================================================================== 36 passed in 0.25s ===========================================================================================================================

After starting the HTTP triggered Function locally with func start :

$ curl http://localhost:7071/api/dev-backup

Dev backup completed%

The Function completed successfully:

$ func start


Azure Functions Core Tools
Core Tools Version:       4.12.0+21d180f3f62481fc8035563798e6d970d8eb0ebb (64-bit)
Function Runtime Version: 4.1048.200.26180

[2026-06-19T16:42:08.137Z] Worker process started and initialized.

Functions:

        dev_ats_backup:  http://localhost:7071/api/dev-backup

        ats_backup: timerTrigger

For detailed output, run func with --verbose flag.
[2026-06-19T16:42:12.019Z] Host lock lease acquired by instance ID '...'.
[2026-06-19T16:42:32.620Z] Executing 'Functions.dev_ats_backup' (Reason='This function was programmatically called via the host APIs.', Id=...)
...
[2026-06-19T16:42:36.310Z] Backup complete. status.json updated.
[2026-06-19T16:42:36.331Z] Executed 'Functions.dev_ats_backup' (Succeeded, Id=1560f61f-da51-4778-a9d0-31b88c7c0a2d, Duration=3726ms)

Due to insufficient rights, I'm not able to deploy the Function on Azure or create a clean Azure Function App with a separate resource group and storage account currently 🚧

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an interactive backup/restore CLI and enhances the core backup/restore logic for Azure Table Storage snapshots (EDM-tagged serialization, pruning strategy updates, restore fidelity via table clearing). It also adds a manual HTTP-triggered backup route to the Azure Functions app and updates dev tooling configuration/dependencies.

Changes:

  • Introduces backup/cli.py interactive workflow for selecting/triggering backup and restore, including snapshot resolution/downloading.
  • Updates backup/core.py to write EDM-tagged snapshots, validate uploads, prune with daily+monthly retention, and restore by clearing the target table before upserting.
  • Adjusts tests and project tooling (ruff/pytest config, dev dependency additions, local snapshot gitignore).

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
backup/core.py Adds EDM-tagged serialization/deserialization, validation, pruning policy, and fidelity-focused restore.
backup/cli.py New interactive CLI for backup/restore orchestration and snapshot download/selection.
function_app.py Adds dev-backup HTTP route for manual triggering.
tests/test_core.py Updates/extends unit tests for new tagging/pruning/restore behaviors.
pyproject.toml Adds dev dependency and adjusts ruff/pytest configuration.
uv.lock Locks new dev dependency transitive graph.
.gitignore Ignores locally-downloaded snapshots/status artifacts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backup/core.py
Comment thread backup/core.py
Comment thread backup/core.py
Comment thread backup/core.py
Comment thread tests/test_core.py
Comment thread backup/cli.py
Comment thread backup/cli.py
Comment thread backup/core.py Outdated
Comment thread backup/core.py
Comment thread backup/cli.py
@ai-mindset ai-mindset self-assigned this Jun 19, 2026
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
ai-mindset and others added 2 commits June 19, 2026 18:26
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
ai-mindset and others added 3 commits June 19, 2026 18:45
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@francisbarton

francisbarton commented Jun 22, 2026

Copy link
Copy Markdown
Member

Hey Eirini @ai-mindset,
I just cloned the repo and switched to this feature branch.
On running uv run pytest I get a test failure:

================================== FAILURES ===================================
____________________ test_main_restore_runs_when_confirmed ____________________

    def test_main_restore_runs_when_confirmed():
        with (
            patch("sys.argv", ["cli"]),
            patch("backup.cli._get_env", return_value="some_table"),
            patch("backup.cli._confirm", side_effect=[False, True]),
            patch("backup.cli._resolve_snapshot", return_value="snap.json"),    
            patch("backup.cli._download_snapshot", return_value="local_snap.json"),
            patch("backup.cli.run_restore") as mock_restore,
        ):
            result = main()

>       assert result == 0
E       assert 1 == 0

tests\test_cli.py:256: AssertionError
---------------------------- Captured stdout call ----------------------------- 
Backup skipped.
Failed to fetch snapshot: [Errno 2] No such file or directory: 'local_snap.json'
=========================== short test summary info =========================== 
FAILED tests/test_cli.py::test_main_restore_runs_when_confirmed - assert 1 == 0 
======================== 1 failed, 35 passed in 1.44s =========================

Please advise, if you can, what I might need to do differently to make this work.

…ses on Windows too

This test failed on Windows. I believe the reason for it is a Windows/Linux/macOS difference in mocking. The dotted-string patch  likely isn't finding the right reference on Windows.
@ai-mindset

ai-mindset commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Hey Eirini @ai-mindset, I just cloned the repo and switched to this feature branch. On running uv run pytest I get a test failure:

================================== FAILURES ===================================
____________________ test_main_restore_runs_when_confirmed ____________________

    def test_main_restore_runs_when_confirmed():
        with (
            patch("sys.argv", ["cli"]),
            patch("backup.cli._get_env", return_value="some_table"),
            patch("backup.cli._confirm", side_effect=[False, True]),
            patch("backup.cli._resolve_snapshot", return_value="snap.json"),    
            patch("backup.cli._download_snapshot", return_value="local_snap.json"),
            patch("backup.cli.run_restore") as mock_restore,
        ):
            result = main()

>       assert result == 0
E       assert 1 == 0

tests\test_cli.py:256: AssertionError
---------------------------- Captured stdout call ----------------------------- 
Backup skipped.
Failed to fetch snapshot: [Errno 2] No such file or directory: 'local_snap.json'
=========================== short test summary info =========================== 
FAILED tests/test_cli.py::test_main_restore_runs_when_confirmed - assert 1 == 0 
======================== 1 failed, 35 passed in 1.44s =========================

Please advise, if you can, what I might need to do differently to make this work.

@francisbarton sorry for the lag, I wanted to find a bit of time to think about this. I believe the reason this specific test fails for you but not for me is a Windows/Linux/macOS difference in mocking. The dotted-string patch patch("backup.cli.run_restore") likely isn't finding the right reference on Windows. Working on making this test OS-independent/more robust, stay tuned and thanks again for your patience!

- Fix mock patching for Windows compatibility (use patch.object for
  imported functions in main())
- Add missing _confirm side_effect for restore confirmation prompt
- Refactor nested test patch into decorator, fix signature mismatch
- Add MODEL_RUNS_TABLE_NAME to env patches matching _get_restore_target()
- Remove superseded EDM type roundtrip test (covered by end-to-end test)
- Add requirements.txt, deployment docs, and status.json updates
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add human-in-the-loop restore script with future GitHub Actions support

4 participants