Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c9285d8
Add --if-exists flag to client setup and refine db dump command
pedroven Jan 28, 2026
0b4f8ac
Fix pg_dump table pattern to include all public tables
pedroven Jan 28, 2026
c52f474
Refactor DB restore process to exclude SCHEMA entry and update pg_dum…
pedroven Jan 29, 2026
da71fa7
Add debug statements to verify TOC entries and row counts during DB r…
pedroven Jan 29, 2026
a3695d4
Remove unnecessary SQL query for user deck relations in client setup
pedroven Jan 29, 2026
e96fa06
Add error handling to AnkiHubHTTPError for response text retrieval
pedroven Jan 29, 2026
94efb28
Add debug output for user data retrieval during test setup
pedroven Jan 29, 2026
5813c52
Refactor SQL query in client setup to select all user fields and simp…
pedroven Jan 29, 2026
3aea728
Add debug output for waffle switch and flag data during server setup
pedroven Jan 29, 2026
4ed7d93
Fix repository name and reference for webapp cloning in CI workflow
pedroven Jan 29, 2026
b3e0ef0
Remove debug output for database and waffle data during server setup
pedroven Jan 29, 2026
0848dd8
Fix pre-commit checks
pedroven Jan 29, 2026
3cd0bfc
Fix mypy errors
pedroven Jan 29, 2026
5a609ca
Merge branch 'main' into INTP-368
pedroven Jan 29, 2026
3936f50
Update CI workflow to checkout 'dev' branch for AnkiHub web app
pedroven Jan 29, 2026
5037c1e
Improve error message in AnkiHubHTTPError to include response text
pedroven Jan 30, 2026
615130f
Update tests/client/test_client.py
pedroven Jan 30, 2026
cd5f726
Enhance error handling in AnkiHubHTTPError with sanitized response de…
pedroven Jan 30, 2026
2003299
Merge branch 'INTP-368' of https://github.com/ankihubsoftware/ankihub…
pedroven Jan 30, 2026
214e575
Merge branch 'main' into INTP-368
pedroven Feb 5, 2026
8ae4971
Refactor type hints in tutorial.py and update media_export import in …
pedroven Feb 5, 2026
e6124af
Condense MainWindowStateLiteral definition in tutorial.py
pedroven Feb 5, 2026
df43940
Refactor MainWindowStateLiteral to use MainWindowState type in tutori…
pedroven Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,15 @@ 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
repository: AnkiHubSoftware/ankihub
ref: dev
path: ankihub_web

Expand Down
46 changes: 45 additions & 1 deletion ankihub/ankihub_client/ankihub_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,58 @@ 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."""

def __init__(self, response: Response):
self.response = response

def __str__(self):
return f"AnkiHub request error: {self.response.status_code} {self.response.reason}"
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):
Expand Down
2 changes: 1 addition & 1 deletion ankihub/entry_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")

Expand Down
14 changes: 7 additions & 7 deletions ankihub/gui/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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,
Expand Down Expand Up @@ -256,7 +256,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
Expand Down Expand Up @@ -526,10 +526,10 @@ 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:
from aqt.main import MainWindowState

def ensure_mw_state(
state: MainWindowState,
) -> Callable[[Callable[..., None]], Callable[..., None]]:
def change_state_and_call_func(func: Callable[..., None], *args: Any, **kwargs: Any) -> None:
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
Expand All @@ -538,7 +538,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:
Expand Down
30 changes: 29 additions & 1 deletion tests/client/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,32 @@ 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"set -o pipefail; pg_restore -l {DB_DUMP_FILE_NAME} | 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",
Expand All @@ -128,6 +153,9 @@ def client_with_server_setup(vcr: VCR, marks: List[str], request: FixtureRequest
f"--username={DB_USERNAME}",
"--format=custom",
"--clean",
"--if-exists",
"-L",
toc_list_path,
"--jobs=4",
DB_DUMP_FILE_NAME,
],
Expand Down
Loading