Skip to content

fix: handle full collection sync - #160

Draft
robertn702 wants to merge 2 commits into
lervag:masterfrom
robertn702:fix/sync-full-sync-159
Draft

robertn702 wants to merge 2 commits into
lervag:masterfrom
robertn702:fix/sync-full-sync-159

Conversation

@robertn702

@robertn702 robertn702 commented Sep 15, 2026 •

Copy link
Copy Markdown

Summary

  • Fail with a clear error when no Anki profile or sync key is available.
  • Read the collection sync response and handle required full downloads, uploads, and conflicts.
  • Use a redirected sync endpoint for the remaining collection and media work.
  • Print server messages instead of discarding them.

Safety

Full uploads and downloads require confirmation. Downloads create and await a local backup before replacing the collection. The collection is reopened after a failed full transfer and closed if automatic sync aborts.

Tests

  • uv run ruff format --check
  • uv run ruff check
  • uv run pyrefly check --baseline pyrefly-baseline.json
  • uv run pytest (38 passed)
  • git diff --check

Closes #159

@robertn702
robertn702 force-pushed the fix/sync-full-sync-159 branch from e9e8907 to c5f071b Compare September 15, 2026 19:10
@robertn702
robertn702 marked this pull request as ready for review September 15, 2026 19:10
Comment thread src/apyanki/anki.py
if output.server_message:
console.print(output.server_message)

if output.required != output.NO_CHANGES:

@lervag lervag Sep 16, 2026 •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Should we add and output.required != output.NORMAL_SYNC?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Or does output.required == output.NORMAL_SYNC mean that a normal sync is required, which seems like a scenario that should not really happen?

Comment thread src/apyanki/anki.py
Comment on lines -173 to +180
return
console.print("[red]Sync requires an Anki profile.")
raise Abort()

hkey = self._profile.get("syncKey")
if not hkey:
return
console.print("[red]Sync requires a sync key.")
raise Abort()

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This is not quite safe and leads to a lot of failed tests and unexpected behaviour. With auto_sync on and no sync key, a successful mutating command now ends in "Aborted!" and exit 1 after the work is done. Erroring is right for an explicit apy sync, but for auto sync path should only warn and skip.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Notice that this is an interesting case where the tests fail on my end because of my own config. We should add a conftest.py file that specifes a non-existing config file. I can do that myself later.

To reproduce this problem, you need to ensure that you have auto_sync enabled in your config.

Comment thread src/apyanki/anki.py Outdated

if output.new_endpoint:
auth.endpoint = output.new_endpoint
self._profile["currentSyncUrl"] = output.new_endpoint

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I don't think this is necessary. It won't be persisted, so I think it ends up having no real effect?

Comment thread src/apyanki/anki.py Outdated
if output.required != output.NO_CHANGES:
if output.required == output.FULL_DOWNLOAD:
upload = False
confirmed = console.confirm(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The confirm dialogs don't work, because they're hidden behind the with Progress on line 190. Needs progress.stop() around the prompt.

@lervag lervag left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks, I appreciate the PR, it addresses a real bug!

On tests/test_sync.py: Thanks for adding these. I'd like to redirect the effort though, because as written they don't constrain much.

The tests construct the object under test with Anki.__new__(Anki) and assign _profile and col by hand, then assert exact call sequences against a hand-written FakeCollection. That describes the implementation rather than the behaviour: assert collection.operations == [("normal", True), ("backup", ...), ("close", None), ...] breaks on any harmless reordering, while real defects pass straight through. There are two real defects in this PR that passes all seven tests:

  1. The confirmation prompt is never visible. It's issued inside the live Progress block, so rich erases it on the next refresh (~100ms). Under a pty you get a spinner and an apparently hung program. Pressing Enter takes the default and aborts. The tests can't see this because console.confirm is monkeypatched away. That is, the test substitutes exactly the part that's broken.

  2. sync() now raises Abort with no profile or sync key, which __exit__ reaches via auto_sync. With "auto_sync": true in ~/.config/apy/apy.json, this branch has 25 failed tests. CI missed it because CI has no apy.json config. And yes, this is a pre-existing gap in the test setup which I'll fix separately.

What I'd find more valuable:

  • Keep the behavioural tests and drop the sequence ones. test_sync_without_key_fails_instead_of_reporting_success and test_sync_conflict_can_be_cancelled encode the actual promise of #159 and are worth having. For the rest, the membership style you already used in test_sync_uploads_when_only_full_upload_is_allowed (assert ("full", ...) in collection.operations) says what matters without freezing the call order.
  • For real coverage, anki ships a sync server: SYNC_BASE=... SYNC_USER1=user:pass python -m anki.syncserver. I checked, it starts and listens on a local port. A fixture that spawns it would let you drive the exact scenario in the issue through the real backend, with no fake to keep in sync with anki's API.

I'll let the second "real coverage" item be optional here, since I can understand it may feel like a larger task. But if you want, feel free to address it.

@robertn702

Copy link
Copy Markdown
Author

@lervag Thanks for the thorough review! I'll get around to updating the PR in the next few days.

@robertn702
robertn702 marked this pull request as draft September 23, 2026 15:41
@lervag

lervag commented Sep 23, 2026

Copy link
Copy Markdown
Owner

@lervag Thanks for the thorough review! I'll get around to updating the PR in the next few days.

I see you've pushed some updates now; let me know when you want me to take another look. And don't hesitate to ask for input or assistance!

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.

apy sync reports success without syncing when a full sync is required

2 participants