# Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations import sys import time import traceback from collections.abc import Iterable, Sequence from threading import current_thread, main_thread from typing import TYPE_CHECKING, Any from weakref import ref from markdown import markdown import anki.buildinfo from anki import _rsbridge, backend_pb2, i18n_pb2 from anki._backend_generated import RustBackendGenerated from anki._fluent import GeneratedTranslations from anki.dbproxy import Row as DBRow from anki.dbproxy import ValueForDB from anki.utils import from_json_bytes, to_json_bytes if TYPE_CHECKING: from anki.collection import FsrsItem from .errors import ( BackendError, BackendIOError, CardTypeError, CustomStudyError, DBError, ExistsError, FilteredDeckError, Interrupted, InvalidInput, NetworkError, NotFoundError, SchedulerUpgradeRequired, SearchError, SyncError, SyncErrorKind, TemplateError, UndoEmpty, ) # the following comment is required to suppress a warning that only shows up # when there are other pylint failures if _rsbridge.buildhash() != anki.buildinfo.buildhash: raise Exception( f"""rsbridge and anki build hashes do not match: {_rsbridge.buildhash()} vs {anki.buildinfo.buildhash}""" ) class RustBackend(RustBackendGenerated): """ Python bindings for Anki's Rust libraries. Please do not access methods on the backend directly - they may be changed or removed at any time. Instead, please use the methods on the collection instead. Eg, don't use col._backend.all_deck_config(), instead use col.decks.all_config() If you need to access a backend method that is not currently accessible via the collection, please send through a pull request that adds a public method. """ @staticmethod def initialize_logging(path: str | None = None) -> None: _rsbridge.initialize_logging(path) def __init__( self, langs: list[str] | None = None, server: bool = False, ) -> None: # pick up global defaults if not provided import anki.lang if langs is None: langs = [anki.lang.current_lang] init_msg = backend_pb2.BackendInit( preferred_langs=langs, server=server, ) self._backend = _rsbridge.open_backend(init_msg.SerializeToString()) @staticmethod def syncserver() -> None: _rsbridge.syncserver() def db_query( self, sql: str, args: Sequence[ValueForDB], first_row_only: bool ) -> list[DBRow]: return self._db_command( dict(kind="query", sql=sql, args=args, first_row_only=first_row_only) ) def db_execute_many(self, sql: str, args: list[list[ValueForDB]]) -> list[DBRow]: return self._db_command(dict(kind="executemany", sql=sql, args=args)) def db_begin(self) -> None: return self._db_command(dict(kind="begin")) def db_commit(self) -> None: return self._db_command(dict(kind="commit")) def db_rollback(self) -> None: return self._db_command(dict(kind="rollback")) def _db_command(self, input: dict[str, Any]) -> Any: bytes_input = to_json_bytes(input) try: return from_json_bytes(self._backend.db_command(bytes_input)) except Exception as error: err_bytes = bytes(error.args[0]) err = backend_pb2.BackendError() err.ParseFromString(err_bytes) raise backend_exception_to_pylib(err) def translate( self, module_index: int, message_index: int, **kwargs: str | int | float ) -> str: args = { k: ( i18n_pb2.TranslateArgValue(str=v) if isinstance(v, str) else i18n_pb2.TranslateArgValue(number=v) ) for k, v in kwargs.items() } return self.translate_string( module_index=module_index, message_index=message_index, args=args ) def format_time_span( self, seconds: Any, context: Any = 2, ) -> str: traceback.print_stack(file=sys.stdout) print( "please use col.format_timespan() instead of col.backend.format_time_span()" ) return self.format_timespan(seconds=seconds, context=context) def compute_params_from_items(self, items: Iterable[FsrsItem]) -> Sequence[float]: return self.compute_fsrs_params_from_items(items).params def benchmark(self, train_set: Iterable[FsrsItem]) -> Sequence[float]: return self.fsrs_benchmark(train_set=train_set) def _run_command(self, service: int, method: int, input: bytes) -> bytes: start = time.time() try: return self._backend.command(service, method, input) except Exception as error: error_bytes = bytes(error.args[0]) finally: elapsed = time.time() - start if current_thread() is main_thread() and elapsed > 0.2: print(f"blocked main thread for {int(elapsed * 1000)}ms:") print("".join(traceback.format_stack())) err = backend_pb2.BackendError() err.ParseFromString(error_bytes) raise backend_exception_to_pylib(err) class Translations(GeneratedTranslations): def __init__(self, backend: ref[RustBackend] | None): self.backend = backend def __call__(self, key: tuple[int, int], **kwargs: Any) -> str: "Mimic the old col.tr / TR interface" if "pytest" not in sys.modules: traceback.print_stack(file=sys.stdout) print("please use tr.message_name() instead of tr(TR.MESSAGE_NAME)") (module, message) = key return self.backend().translate( module_index=module, message_index=message, **kwargs ) def _translate( self, module: int, message: int, args: dict[str, str | int | float] ) -> str: return self.backend().translate( module_index=module, message_index=message, **args ) def backend_exception_to_pylib(err: backend_pb2.BackendError) -> Exception: kind = backend_pb2.BackendError val = err.kind help_page = err.help_page if err.HasField("help_page") else None context = err.context if err.context else None backtrace = err.backtrace if err.backtrace else None if val == kind.INTERRUPTED: return Interrupted(err.message, help_page, context, backtrace) elif val == kind.NETWORK_ERROR: return NetworkError(err.message, help_page, context, backtrace) elif val == kind.SYNC_AUTH_ERROR: return SyncError(err.message, help_page, context, backtrace, SyncErrorKind.AUTH) elif val == kind.SYNC_OTHER_ERROR: return SyncError( err.message, help_page, context, backtrace, SyncErrorKind.OTHER ) elif val == kind.IO_ERROR: return BackendIOError(err.message, help_page, context, backtrace) elif val == kind.DB_ERROR: return DBError(err.message, help_page, context, backtrace) elif val == kind.CARD_TYPE_ERROR: return CardTypeError(err.message, help_page, context, backtrace) elif val == kind.TEMPLATE_PARSE: return TemplateError(err.message, help_page, context, backtrace) elif val == kind.INVALID_INPUT: return InvalidInput(err.message, help_page, context, backtrace) elif val == kind.JSON_ERROR: return BackendError(err.message, help_page, context, backtrace) elif val == kind.NOT_FOUND_ERROR: return NotFoundError(err.message, help_page, context, backtrace) elif val == kind.EXISTS: return ExistsError(err.message, help_page, context, backtrace) elif val == kind.FILTERED_DECK_ERROR: return FilteredDeckError(err.message, help_page, context, backtrace) elif val == kind.PROTO_ERROR: return BackendError(err.message, help_page, context, backtrace) elif val == kind.SEARCH_ERROR: return SearchError(markdown(err.message), help_page, context, backtrace) elif val == kind.UNDO_EMPTY: return UndoEmpty(err.message, help_page, context, backtrace) elif val == kind.CUSTOM_STUDY_ERROR: return CustomStudyError(err.message, help_page, context, backtrace) elif val == kind.SCHEDULER_UPGRADE_REQUIRED: return SchedulerUpgradeRequired(err.message, help_page, context, backtrace) else: # sadly we can't do exhaustiveness checking on protobuf enums # assert_exhaustive(val) return BackendError(err.message, help_page, context, backtrace)