fix(import): return descriptive error messages and roll back failed imports - #176
TimeToBuildBob wants to merge 7 commits into
Conversation
Previously the import endpoint returned null on success (displayed as
"null" in the web UI) and a generic 500 on failure. This caused confusion
about whether the import worked and gave no actionable error message.
- api.py: raise a descriptive exception when importing a bucket that
already exists (resolves the TODO comment)
- rest.py: wrap import logic in try/except so errors surface as HTTP 400
with a human-readable message; return {"message": "Import successful"}
on success instead of null
Closes ActivityWatch/activitywatch#394
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Git-Session-Id: 2625
If import_bucket fails partway through a multi-bucket export, previously- committed buckets are now deleted before re-raising, so the database is never left in a partially-imported state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Git-Session-Id: 2625
…s 500 - Check all bucket IDs before writing, so a rejected import writes nothing. - On failure, delete every bucket this import created, including one whose events failed after the bucket was created (previously left behind), and go through ServerAPI.delete_bucket so the query cache is invalidated. - Run import_all under the write lock. - Raise ValueError for an existing bucket and return 400 only for client errors (KeyError/TypeError/ValueError); unexpected failures stay 500 and are no longer logged with a traceback for routine user mistakes. - Add tests/test_import.py covering success, duplicate, rollback, bad events, missing key and malformed JSON; include it in make test. Git-Session-Id: 2625
|
|
CI-green and mergeable — waiting only on a maintainer click. This PR is ready to merge, but the bot has pull-only access to this repo and can't self-merge — surfacing it here so it isn't lost. The monitoring loop will stop re-flagging it now that this note is posted. |
… back multipart batches - A bucket ID repeated within one export is rejected up front (previously the rollback deleted it twice and returned 404). - 'buckets' that is not an object returns 400 instead of an AttributeError 500. - Multipart uploads are imported as one batch, so a failing file rolls back the files before it. Git-Session-Id: 2625
|
@greptileai review |
🤖 AI code reviewThis PR reworks the import endpoint in aw_server. It adds an up-front duplicate-bucket and duplicate-ID check in ServerAPI.import_all, wraps import_all in the write lock, and on failure rolls back every bucket created by the import via delete_bucket, raising ImportRollbackError when rollback is incomplete. The REST layer now catches ImportRollbackError as a 500 and KeyError/TypeError/ValueError as a 400 with a JSON message, and multipart uploads are merged into one batch. It adds tests/test_import.py and wires it into make test. Safe to merge — no P0/P1 findingsConfidence 5/5 ✅ No findings. The diff looks correct to me on this pass. Files changed (4) — the diff as I read it
Previous review passes
Reviewed Maintainer commands
|
Track seen bucket IDs in a set instead of an O(n^2) list.count() scan that ran while import_all held the write lock shared by bucket and event writes. Wrap each rollback deletion in its own try/except so a failure deleting one bucket cannot abort the rollback of the others or replace the original import error (which would surface as a 500 instead of the intended 400). Adds a regression test that mocks delete_bucket to raise during rollback. Git-Session-Id: 0fcffd6b-7aa2-5e32-9c7f-8dbf57971406
|
@greptileai review |
If deleting a bucket during rollback fails, the bucket remains stored while the endpoint returned a client-fault 400 saying the import failed. A retry is then rejected as a duplicate. Track the buckets that could not be removed and raise a RuntimeError (500) spelling out the partial state instead of re-raising the original error. Rollback still attempts every bucket, so one delete failure does not abort the others. Tests updated: one asserts both delete attempts happen and the response is a 500, one asserts the surviving bucket and the rolled-back peer. Git-Session-Id: 0fcffd6b-7aa2-5e32-9c7f-8dbf57971406
|
@greptileai review |
| raise RuntimeError( | ||
| "Import failed and rollback could not remove bucket(s) " | ||
| f"{failed_rollbacks!r}; they remain stored. " | ||
| f"Original error: {import_error!r}" | ||
| ) from import_error |
There was a problem hiding this comment.
Rollback details never reach clients
When deleting a bucket fails during rollback, this RuntimeError names the buckets left stored. The import endpoint does not catch it, so the client receives a generic 500 response instead of those details. The client cannot tell which buckets must be removed before retrying.
Knowledge Base Used: REST request handling
There was a problem hiding this comment.
Fixed in 934f7b8 — import_all now raises a dedicated ImportRollbackError, and the REST handler catches it and returns its message as a JSON 500 (instead of a generic HTML 500) naming the buckets that remain stored. The client now learns which buckets to delete before retrying.
Both rollback tests now assert the remaining bucket IDs appear in the response body, and they fail without this change (verified by reverting rest.py).
When a rollback cannot delete a bucket it created, raise a dedicated ImportRollbackError and have the REST handler return its message as a JSON 500 instead of a generic HTML 500. The client now learns which buckets remain stored and must be deleted before retrying. Regression tests assert the remaining bucket IDs appear in the response body. Git-Session-Id: 51b7eeab-8209-54d4-8c8c-8516b66a2398
|
@greptileai review |
Carries #165 by @behdadmansouri. Thanks for this! It resolves the
# TODOinimport_bucketand replaces the import endpoint's HTML 500s with JSON messages. This is the Python-server half of ActivityWatch/activitywatch#394; the webui half (ActivityWatch/aw-webui#1019) already rendersmessage.#165 conflicted with the query-cache change (#174), and I don't have push access to the fork. So this PR is #165 rebased, with both original commits kept under @behdadmansouri's authorship, plus one follow-up commit:
imported, so it was left behind. Bucket IDs are now all checked up front. On failure, every bucket this import created is deleted, viaServerAPI.delete_bucketso the query cache is invalidated.import_allnow runs under the write lock.ValueError. OnlyKeyError/TypeError/ValueErrorbecome a 400. Anything else stays a 500 and is not reported as a client error. Routine user mistakes are logged as a one-line warning instead of a traceback (both of Greptile's P2s on fix(import): return descriptive error messages and success response #165).buckets, is rejected with a 400. Multipart uploads are imported as one batch, so a failing file rolls back the files before it.tests/test_import.py(9 cases, incl. repeated IDs, non-objectbuckets, multipart rollback), added tomake test.Live check (
aw-server --testing, peewee storage, clean data dir)null200{"message": "Import successful"}200IntegrityError: UNIQUE constraint failedImport failed: Bucket 'carry-a' already exists. Delete it first or rename the bucket before importing.bucketskeyKeyErrorImport failed: missing field 'buckets'Import failed: Event.__init__() got an unexpected keyword argument 'not', rolled backpytest tests/test_server.py tests/test_profile.py tests/test_profile_config.py tests/test_query_cache.py tests/test_import.py→ 75 passed. ruff (pinned 0.15.22) and black are clean. mypy reports the same 9 errors as on master.Differences from aw-server-rust
{"message": ...}with a 4xx/5xx status.{"message": "Import successful"}. aw-webui handles both.Separate problem found while testing (not caused by this PR)
aw-core0.5.18 allowspeewee>=3,<5. Under peewee 4.5.2, the peewee storage is broken on master.POST /api/0/buckets/<id>returns 500, and the storedcreatedthen makes everyGET /api/0/buckets/fail (iso8601.ParseError: expected string or bytes-like object, got 'datetime.datetime'inBucketModel.json). This is ActivityWatch/aw-core#157, already fixed by ActivityWatch/aw-core#158, but that fix landed after v0.5.18 and isn't released yet. The lockfile pins peewee 3.17.0, so locked installs are fine. An unpinnedpip install aw-serverwould pull peewee 4 until aw-core 0.5.19 ships. The live check above used peewee 3.17.0.Supersedes #165.