From c9285d8cfdf0d6ab89d5d1ac0b59e0f9f24f1015 Mon Sep 17 00:00:00 2001 From: Pedro Date: Wed, 28 Jan 2026 16:03:52 -0300 Subject: [PATCH 01/20] Add --if-exists flag to client setup and refine db dump command --- tests/client/test_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index d6c0f2934..bb13bec9b 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -128,6 +128,7 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest f"--username={DB_USERNAME}", "--format=custom", "--clean", + "--if-exists", "--jobs=4", DB_DUMP_FILE_NAME, ], @@ -233,7 +234,7 @@ def create_db_dump_if_not_exists() -> None: "-c", ( f"pg_dump --dbname={DB_NAME} --username={DB_USERNAME} " - "--format=custom --schema=public " + "--format=custom -t 'public.*' " f"> {DB_DUMP_FILE_NAME}" ), ], From 0b4f8ac5932e4e350180ffeb99646c9e5e659ba3 Mon Sep 17 00:00:00 2001 From: Pedro Date: Wed, 28 Jan 2026 16:53:52 -0300 Subject: [PATCH 02/20] Fix pg_dump table pattern to include all public tables --- tests/client/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index bb13bec9b..db78f893d 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -234,7 +234,7 @@ def create_db_dump_if_not_exists() -> None: "-c", ( f"pg_dump --dbname={DB_NAME} --username={DB_USERNAME} " - "--format=custom -t 'public.*' " + "--format=custom -t 'public.%' " f"> {DB_DUMP_FILE_NAME}" ), ], From c52f4740d5eab59fcde1430313d423d65e51d142 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 10:35:10 -0300 Subject: [PATCH 03/20] Refactor DB restore process to exclude SCHEMA entry and update pg_dump command for improved behavior --- tests/client/test_client.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index db78f893d..b76c95a62 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -116,7 +116,35 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest if not is_playback_mode(vcr, request): create_db_dump_if_not_exists() - # Restore DB from dump + # Restore DB from dump using a filtered TOC list that excludes the + # SCHEMA entry. pg_restore --clean tries to DROP SCHEMA public which + # fails when extensions (pg_trgm, vector) depend on it. By filtering + # out the SCHEMA entry we still get full --clean behavior for tables + # (drop + recreate) without touching the schema itself. + toc_list_path = "/tmp/restore_toc.list" + result = subprocess.run( + [ + "docker", + "exec", + "-i", + DB_CONTAINER_NAME, + "bash", + "-c", + ( + f"pg_restore -l {DB_DUMP_FILE_NAME}" + f" | grep -v ' SCHEMA ' > {toc_list_path}" + ), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + report_command_result( + command_name="pg_restore TOC list", + result=result, + raise_on_error=True, + ) + result = subprocess.run( [ "docker", @@ -129,6 +157,7 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest "--format=custom", "--clean", "--if-exists", + "-L", toc_list_path, "--jobs=4", DB_DUMP_FILE_NAME, ], @@ -234,7 +263,7 @@ def create_db_dump_if_not_exists() -> None: "-c", ( f"pg_dump --dbname={DB_NAME} --username={DB_USERNAME} " - "--format=custom -t 'public.%' " + "--format=custom --schema=public " f"> {DB_DUMP_FILE_NAME}" ), ], From da71fa74f15d51813ce3c84c1f5718978d6ad530 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 11:20:52 -0300 Subject: [PATCH 04/20] Add debug statements to verify TOC entries and row counts during DB restore and fixture creation --- tests/client/test_client.py | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index b76c95a62..063308ec2 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -145,6 +145,23 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest raise_on_error=True, ) + # Debug: print the filtered TOC list to verify what will be restored + toc_result = subprocess.run( + [ + "docker", + "exec", + "-i", + DB_CONTAINER_NAME, + "bash", + "-c", + f"wc -l < {toc_list_path} && grep 'TABLE DATA' {toc_list_path}", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + print(f"Filtered TOC list (TABLE DATA entries):\n{toc_result.stdout}") + result = subprocess.run( [ "docker", @@ -173,6 +190,33 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest raise_on_error=False, ) + # Debug: check row counts in key tables after restore + result = subprocess.run( + [ + "docker", + "exec", + "-i", + DB_CONTAINER_NAME, + "psql", + f"--dbname={DB_NAME}", + f"--username={DB_USERNAME}", + "-c", + ( + "SELECT 'users_user' AS tbl, COUNT(*) FROM users_user " + "UNION ALL SELECT 'decks_deck', COUNT(*) FROM decks_deck " + "UNION ALL SELECT 'decks_userdeckrelation', COUNT(*) FROM decks_userdeckrelation " + "UNION ALL SELECT 'knox_authtoken', COUNT(*) FROM knox_authtoken " + "UNION ALL SELECT 'decks_deckmedia', COUNT(*) FROM decks_deckmedia;" + ), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + print(f"DB row counts after restore:\n{result.stdout}") + if result.stderr: + print(f"DB row counts stderr: {result.stderr}") + _wait_for_server( api_url=LOCAL_API_URL, timeout=30.0, @@ -253,6 +297,32 @@ def create_db_dump_if_not_exists() -> None: ) report_command_result(command_name="db setup", result=result, raise_on_error=True) + # Debug: check row counts after fixture creation (before dump) + result = subprocess.run( + [ + "docker", + "exec", + "-i", + DB_CONTAINER_NAME, + "psql", + f"--dbname={DB_NAME}", + f"--username={DB_USERNAME}", + "-c", + ( + "SELECT 'users_user' AS tbl, COUNT(*) FROM users_user " + "UNION ALL SELECT 'decks_deck', COUNT(*) FROM decks_deck " + "UNION ALL SELECT 'knox_authtoken', COUNT(*) FROM knox_authtoken " + "UNION ALL SELECT 'decks_deckmedia', COUNT(*) FROM decks_deckmedia;" + ), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + print(f"DB row counts after fixture creation (before dump):\n{result.stdout}") + if result.stderr: + print(f"DB row counts stderr: {result.stderr}") + # Dump the DB to a file to be able to restore it before each test result = subprocess.run( [ From a3695d468cec35f68968fe2142e0a78c723dc4c6 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 11:28:02 -0300 Subject: [PATCH 05/20] Remove unnecessary SQL query for user deck relations in client setup --- tests/client/test_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 063308ec2..513032ddd 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -204,7 +204,6 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest ( "SELECT 'users_user' AS tbl, COUNT(*) FROM users_user " "UNION ALL SELECT 'decks_deck', COUNT(*) FROM decks_deck " - "UNION ALL SELECT 'decks_userdeckrelation', COUNT(*) FROM decks_userdeckrelation " "UNION ALL SELECT 'knox_authtoken', COUNT(*) FROM knox_authtoken " "UNION ALL SELECT 'decks_deckmedia', COUNT(*) FROM decks_deckmedia;" ), From e96fa06223ce259035a7aff4abc0c0d7cc047ea0 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 13:43:50 -0300 Subject: [PATCH 06/20] Add error handling to AnkiHubHTTPError for response text retrieval --- ankihub/ankihub_client/ankihub_client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ankihub/ankihub_client/ankihub_client.py b/ankihub/ankihub_client/ankihub_client.py index 49c8b002b..d6333ba2b 100644 --- a/ankihub/ankihub_client/ankihub_client.py +++ b/ankihub/ankihub_client/ankihub_client.py @@ -141,6 +141,11 @@ def __init__(self, response: Response): self.response = response def __str__(self): + try: + response_text = self.response.text + except Exception: + response_text = "Unable to read response content" + print("Response text:", response_text) return f"AnkiHub request error: {self.response.status_code} {self.response.reason}" From 94efb2833ed8c7d924f886e8607629a23342ae88 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 14:00:21 -0300 Subject: [PATCH 07/20] Add debug output for user data retrieval during test setup --- tests/client/test_client.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 513032ddd..c6f77819e 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -216,6 +216,32 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest if result.stderr: print(f"DB row counts stderr: {result.stderr}") + # Debug: dump full user data for test users + result = subprocess.run( + [ + "docker", + "exec", + "-i", + DB_CONTAINER_NAME, + "psql", + f"--dbname={DB_NAME}", + f"--username={DB_USERNAME}", + "-c", + ( + "SELECT id, username, email, is_active, is_staff, is_superuser, " + "customer_id, trial_started_at, agreed_to_terms, " + "external_course_access_status " + "FROM users_user;" + ), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + print(f"User data for test1/test2 after restore:\n{result.stdout}") + if result.stderr: + print(f"User data stderr: {result.stderr}") + _wait_for_server( api_url=LOCAL_API_URL, timeout=30.0, From 5813c5290c287db0832e33ae1015cbd850c5776a Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 14:09:24 -0300 Subject: [PATCH 08/20] Refactor SQL query in client setup to select all user fields and simplify debug output --- tests/client/test_client.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index c6f77819e..28b62f802 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -227,18 +227,13 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest f"--dbname={DB_NAME}", f"--username={DB_USERNAME}", "-c", - ( - "SELECT id, username, email, is_active, is_staff, is_superuser, " - "customer_id, trial_started_at, agreed_to_terms, " - "external_course_access_status " - "FROM users_user;" - ), + "SELECT * FROM users_user;", ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) - print(f"User data for test1/test2 after restore:\n{result.stdout}") + print(f"User data after restore:\n{result.stdout}") if result.stderr: print(f"User data stderr: {result.stderr}") From 3aea7286436dce51e0830e44ccf977477c2e7737 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 14:39:46 -0300 Subject: [PATCH 09/20] Add debug output for waffle switch and flag data during server setup --- tests/client/test_client.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 28b62f802..768d139f3 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -237,6 +237,32 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest if result.stderr: print(f"User data stderr: {result.stderr}") + # Debug: dump waffle switch and flag data + result = subprocess.run( + [ + "docker", + "exec", + "-i", + DB_CONTAINER_NAME, + "psql", + f"--dbname={DB_NAME}", + f"--username={DB_USERNAME}", + "-c", + ( + "SELECT * FROM waffle_switch;" + "SELECT * FROM waffle_flag;" + "SELECT * FROM waffle_flag_users;" + "SELECT * FROM waffle_flag_groups;" + ), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + print(f"Waffle data after restore:\n{result.stdout}") + if result.stderr: + print(f"Waffle data stderr: {result.stderr}") + _wait_for_server( api_url=LOCAL_API_URL, timeout=30.0, From 4ed7d93aa60ea8a20f85131da7b5cafc48ec5054 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 16:12:26 -0300 Subject: [PATCH 10/20] Fix repository name and reference for webapp cloning in CI workflow --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d7a148f2..6cd4019d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,16 +98,16 @@ jobs: aqt_dependency_group: aqt qt_package: pyqt6 - - name: Enable cloning web app repo + - name: Enable cloning webapp repo uses: webfactory/ssh-agent@v0.5.4 with: ssh-private-key: ${{ secrets.ANKIHUB_SSH_PRIVATE_KEY }} - - name: Checkout ankipalace/ankihub web app repo + - name: Checkout AnkiHubSoftware/ankihub web app repo uses: actions/checkout@v4 with: - repository: ankipalace/ankihub - ref: dev + repository: AnkiHubSoftware/ankihub + ref: INTP-368 path: ankihub_web - name: Set up AnkiHub env From b3e0ef0e8fcac0cfd16062e29f8c336dc88302d6 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 16:29:46 -0300 Subject: [PATCH 11/20] Remove debug output for database and waffle data during server setup --- tests/client/test_client.py | 116 ------------------------------------ 1 file changed, 116 deletions(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 768d139f3..b76c95a62 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -145,23 +145,6 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest raise_on_error=True, ) - # Debug: print the filtered TOC list to verify what will be restored - toc_result = subprocess.run( - [ - "docker", - "exec", - "-i", - DB_CONTAINER_NAME, - "bash", - "-c", - f"wc -l < {toc_list_path} && grep 'TABLE DATA' {toc_list_path}", - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - print(f"Filtered TOC list (TABLE DATA entries):\n{toc_result.stdout}") - result = subprocess.run( [ "docker", @@ -190,79 +173,6 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest raise_on_error=False, ) - # Debug: check row counts in key tables after restore - result = subprocess.run( - [ - "docker", - "exec", - "-i", - DB_CONTAINER_NAME, - "psql", - f"--dbname={DB_NAME}", - f"--username={DB_USERNAME}", - "-c", - ( - "SELECT 'users_user' AS tbl, COUNT(*) FROM users_user " - "UNION ALL SELECT 'decks_deck', COUNT(*) FROM decks_deck " - "UNION ALL SELECT 'knox_authtoken', COUNT(*) FROM knox_authtoken " - "UNION ALL SELECT 'decks_deckmedia', COUNT(*) FROM decks_deckmedia;" - ), - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - print(f"DB row counts after restore:\n{result.stdout}") - if result.stderr: - print(f"DB row counts stderr: {result.stderr}") - - # Debug: dump full user data for test users - result = subprocess.run( - [ - "docker", - "exec", - "-i", - DB_CONTAINER_NAME, - "psql", - f"--dbname={DB_NAME}", - f"--username={DB_USERNAME}", - "-c", - "SELECT * FROM users_user;", - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - print(f"User data after restore:\n{result.stdout}") - if result.stderr: - print(f"User data stderr: {result.stderr}") - - # Debug: dump waffle switch and flag data - result = subprocess.run( - [ - "docker", - "exec", - "-i", - DB_CONTAINER_NAME, - "psql", - f"--dbname={DB_NAME}", - f"--username={DB_USERNAME}", - "-c", - ( - "SELECT * FROM waffle_switch;" - "SELECT * FROM waffle_flag;" - "SELECT * FROM waffle_flag_users;" - "SELECT * FROM waffle_flag_groups;" - ), - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - print(f"Waffle data after restore:\n{result.stdout}") - if result.stderr: - print(f"Waffle data stderr: {result.stderr}") - _wait_for_server( api_url=LOCAL_API_URL, timeout=30.0, @@ -343,32 +253,6 @@ def create_db_dump_if_not_exists() -> None: ) report_command_result(command_name="db setup", result=result, raise_on_error=True) - # Debug: check row counts after fixture creation (before dump) - result = subprocess.run( - [ - "docker", - "exec", - "-i", - DB_CONTAINER_NAME, - "psql", - f"--dbname={DB_NAME}", - f"--username={DB_USERNAME}", - "-c", - ( - "SELECT 'users_user' AS tbl, COUNT(*) FROM users_user " - "UNION ALL SELECT 'decks_deck', COUNT(*) FROM decks_deck " - "UNION ALL SELECT 'knox_authtoken', COUNT(*) FROM knox_authtoken " - "UNION ALL SELECT 'decks_deckmedia', COUNT(*) FROM decks_deckmedia;" - ), - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - print(f"DB row counts after fixture creation (before dump):\n{result.stdout}") - if result.stderr: - print(f"DB row counts stderr: {result.stderr}") - # Dump the DB to a file to be able to restore it before each test result = subprocess.run( [ From 0848dd81664834b09f8ea727e4c7d5abc266b7cc Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 16:37:20 -0300 Subject: [PATCH 12/20] Fix pre-commit checks --- tests/client/test_client.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index b76c95a62..9142ed434 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -130,10 +130,7 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest DB_CONTAINER_NAME, "bash", "-c", - ( - f"pg_restore -l {DB_DUMP_FILE_NAME}" - f" | grep -v ' SCHEMA ' > {toc_list_path}" - ), + (f"pg_restore -l {DB_DUMP_FILE_NAME} | grep -v ' SCHEMA ' > {toc_list_path}"), ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -157,7 +154,8 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest "--format=custom", "--clean", "--if-exists", - "-L", toc_list_path, + "-L", + toc_list_path, "--jobs=4", DB_DUMP_FILE_NAME, ], From 3cd0bfcbb719100c67665092e89c002f77021010 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 16:48:35 -0300 Subject: [PATCH 13/20] Fix mypy errors --- ankihub/gui/tutorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ankihub/gui/tutorial.py b/ankihub/gui/tutorial.py index 58448c336..de6e59b94 100644 --- a/ankihub/gui/tutorial.py +++ b/ankihub/gui/tutorial.py @@ -241,7 +241,7 @@ class TutorialStep: hidden_callback: Optional[Callable[[], None]] = None next_callback: Optional[Callable[[Callable[[], None]], None]] = None next_label: str = "Next" - back_callback: Optional[Callable[[], None]] = None + back_callback: Optional[Callable[[Callable[[], None]], None]] = None back_label: str = "Back" block_target_click: bool = False auto_advance: bool = True From 3936f50b8954f84b3afc2f585364a6f754608f33 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 29 Jan 2026 17:15:58 -0300 Subject: [PATCH 14/20] Update CI workflow to checkout 'dev' branch for AnkiHub web app --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cd4019d2..628e4a97f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: uses: actions/checkout@v4 with: repository: AnkiHubSoftware/ankihub - ref: INTP-368 + ref: dev path: ankihub_web - name: Set up AnkiHub env From 5037c1e8d772444e424e8027c23dfbfbb905902b Mon Sep 17 00:00:00 2001 From: Pedro Date: Fri, 30 Jan 2026 10:54:47 -0300 Subject: [PATCH 15/20] Improve error message in AnkiHubHTTPError to include response text --- ankihub/ankihub_client/ankihub_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ankihub/ankihub_client/ankihub_client.py b/ankihub/ankihub_client/ankihub_client.py index d6333ba2b..7d286d046 100644 --- a/ankihub/ankihub_client/ankihub_client.py +++ b/ankihub/ankihub_client/ankihub_client.py @@ -145,8 +145,7 @@ def __str__(self): response_text = self.response.text except Exception: response_text = "Unable to read response content" - print("Response text:", response_text) - return f"AnkiHub request error: {self.response.status_code} {self.response.reason}" + return f"AnkiHub request error: {self.response.status_code} {self.response.reason}\n{response_text}" class AnkiHubRequestException(Exception): From 615130fb90e71a1567ac07283dbb0830bd9d6773 Mon Sep 17 00:00:00 2001 From: Pedro Costa Date: Fri, 30 Jan 2026 11:48:11 -0300 Subject: [PATCH 16/20] Update tests/client/test_client.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/client/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 9142ed434..0d321664c 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -130,7 +130,7 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest DB_CONTAINER_NAME, "bash", "-c", - (f"pg_restore -l {DB_DUMP_FILE_NAME} | grep -v ' SCHEMA ' > {toc_list_path}"), + (f"set -o pipefail; pg_restore -l {DB_DUMP_FILE_NAME} | grep -v ' SCHEMA ' > {toc_list_path}"), ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, From cd5f7269a0e2f4fdbbf00674fea316f63a936b42 Mon Sep 17 00:00:00 2001 From: Pedro Date: Fri, 30 Jan 2026 11:52:37 -0300 Subject: [PATCH 17/20] Enhance error handling in AnkiHubHTTPError with sanitized response details --- ankihub/ankihub_client/ankihub_client.py | 50 +++++++++++++++++++++--- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/ankihub/ankihub_client/ankihub_client.py b/ankihub/ankihub_client/ankihub_client.py index 7d286d046..e0ec7225f 100644 --- a/ankihub/ankihub_client/ankihub_client.py +++ b/ankihub/ankihub_client/ankihub_client.py @@ -134,6 +134,44 @@ def _should_retry_for_response(response: Response) -> bool: RETRY_CONDITION = retry_if_result(_should_retry_for_response) | retry_if_exception_type(REQUEST_RETRY_EXCEPTION_TYPES) +def _sanitized_response_detail(response: Response) -> Optional[str]: + """Extract a safe, truncated detail string from an HTTP response body. + + Prefers structured JSON fields ("detail", "message") when available, + otherwise falls back to a truncated version of the raw body text. + Returns None if the body cannot be read at all. + """ + try: + content_type = (response.headers.get("Content-Type") or "").lower() + except Exception: + content_type = "" + + try: + body_text = response.text + except Exception: + return None + + if not body_text: + return None + + # Prefer structured error information from JSON responses. + if "application/json" in content_type: + try: + data = response.json() + if isinstance(data, dict): + detail = data.get("detail") or data.get("message") + if isinstance(detail, str): + return detail + except Exception: + pass + + # Fall back to a truncated version of the body. + max_len = 500 + if len(body_text) > max_len: + return body_text[:max_len] + "... [truncated]" + return body_text + + class AnkiHubHTTPError(Exception): """An unexpected HTTP code was returned in response to a request by the AnkiHub client.""" @@ -141,11 +179,13 @@ def __init__(self, response: Response): self.response = response def __str__(self): - try: - response_text = self.response.text - except Exception: - response_text = "Unable to read response content" - return f"AnkiHub request error: {self.response.status_code} {self.response.reason}\n{response_text}" + summary = f"AnkiHub request error: {self.response.status_code} {self.response.reason}" + detail = _sanitized_response_detail(self.response) + if detail is None: + return f"{summary}\nUnable to read response content" + if detail: + return f"{summary}\n{detail}" + return summary class AnkiHubRequestException(Exception): From 8ae4971a65dbb7b8bc73826b62d16943933a3efc Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 5 Feb 2026 13:48:30 -0300 Subject: [PATCH 18/20] Refactor type hints in tutorial.py and update media_export import in entry_point.py --- ankihub/entry_point.py | 2 +- ankihub/gui/tutorial.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ankihub/entry_point.py b/ankihub/entry_point.py index e3ad273d4..785a780cf 100644 --- a/ankihub/entry_point.py +++ b/ankihub/entry_point.py @@ -252,7 +252,7 @@ def _general_setup() -> None: _trigger_addon_update_check() LOGGER.info("Triggered add-on update check.") - from . import media_export # noqa: F401 + from . import media_export # type: ignore[attr-defined] # noqa: F401 LOGGER.info("Loaded media_export.") diff --git a/ankihub/gui/tutorial.py b/ankihub/gui/tutorial.py index 2fc4ce34f..cc024c023 100644 --- a/ankihub/gui/tutorial.py +++ b/ankihub/gui/tutorial.py @@ -3,7 +3,7 @@ from asyncio.futures import Future from dataclasses import dataclass from functools import cached_property, partial -from typing import Any, Callable, Optional, Union +from typing import Any, Callable, Literal, Optional, Union import aqt from aqt import gui_hooks @@ -526,8 +526,15 @@ def next(self) -> None: self.show_current() -def ensure_mw_state(state: str) -> Callable[[...], None]: - def change_state_and_call_func(func: Callable[[...], None], *args: Any, **kwargs: Any) -> None: +MainWindowStateLiteral = Literal[ + "startup", "deckBrowser", "overview", "review", "resetRequired", "profileManager" +] + + +def ensure_mw_state( + state: MainWindowStateLiteral, +) -> Callable[[Callable[..., None]], Callable[..., None]]: + def change_state_and_call_func(func: Callable[..., None], *args: Any, **kwargs: Any) -> None: from aqt.main import MainWindowState def on_state_did_change(old_state: MainWindowState, new_state: MainWindowState) -> None: @@ -538,7 +545,7 @@ def on_state_did_change(old_state: MainWindowState, new_state: MainWindowState) gui_hooks.state_did_change.append(on_state_did_change) aqt.mw.moveToState(state) - def decorated_func(func: Callable[[...], None]) -> Callable[[...], None]: + def decorated_func(func: Callable[..., None]) -> Callable[..., None]: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> None: if aqt.mw.state != state: From e6124af5fc9c1b544a8406b128ef725cd7a5a180 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 5 Feb 2026 13:57:37 -0300 Subject: [PATCH 19/20] Condense MainWindowStateLiteral definition in tutorial.py --- ankihub/gui/tutorial.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ankihub/gui/tutorial.py b/ankihub/gui/tutorial.py index cc024c023..27da64ed1 100644 --- a/ankihub/gui/tutorial.py +++ b/ankihub/gui/tutorial.py @@ -526,9 +526,7 @@ def next(self) -> None: self.show_current() -MainWindowStateLiteral = Literal[ - "startup", "deckBrowser", "overview", "review", "resetRequired", "profileManager" -] +MainWindowStateLiteral = Literal["startup", "deckBrowser", "overview", "review", "resetRequired", "profileManager"] def ensure_mw_state( From df4394069d7e97bee981d8ab8f955f40b09d8e32 Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 5 Feb 2026 14:43:45 -0300 Subject: [PATCH 20/20] Refactor MainWindowStateLiteral to use MainWindowState type in tutorial.py --- ankihub/gui/tutorial.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/ankihub/gui/tutorial.py b/ankihub/gui/tutorial.py index 27da64ed1..4850e7d28 100644 --- a/ankihub/gui/tutorial.py +++ b/ankihub/gui/tutorial.py @@ -3,12 +3,12 @@ from asyncio.futures import Future from dataclasses import dataclass from functools import cached_property, partial -from typing import Any, Callable, Literal, Optional, Union +from typing import Any, Callable, Optional, Union import aqt from aqt import gui_hooks from aqt.editor import Editor -from aqt.main import AnkiQt +from aqt.main import AnkiQt, MainWindowState from aqt.overview import Overview, OverviewBottomBar from aqt.qt import ( QPoint, @@ -526,15 +526,10 @@ def next(self) -> None: self.show_current() -MainWindowStateLiteral = Literal["startup", "deckBrowser", "overview", "review", "resetRequired", "profileManager"] - - def ensure_mw_state( - state: MainWindowStateLiteral, + state: MainWindowState, ) -> Callable[[Callable[..., None]], Callable[..., None]]: def change_state_and_call_func(func: Callable[..., None], *args: Any, **kwargs: Any) -> None: - from aqt.main import MainWindowState - def on_state_did_change(old_state: MainWindowState, new_state: MainWindowState) -> None: gui_hooks.state_did_change.remove(on_state_did_change) # Some delay appears to be required for the toolbar