diff --git a/pylib/anki/_backend/__init__.py b/pylib/anki/_backend/__init__.py index e01a6bd99..718fbbf48 100644 --- a/pylib/anki/_backend/__init__.py +++ b/pylib/anki/_backend/__init__.py @@ -106,12 +106,22 @@ class RustBackend(RustBackendGenerated): def translate( self, module_index: int, message_index: int, **kwargs: str | int | float ) -> str: - return self.translate_string( - translate_string_in( - module_index=module_index, message_index=message_index, **kwargs - ) + args = { + k: i18n_pb2.TranslateArgValue(str=v) + if isinstance(v, str) + else i18n_pb2.TranslateArgValue(number=v) + for k, v in kwargs.items() + } + + input = i18n_pb2.TranslateStringRequest( + module_index=module_index, + message_index=message_index, + args=args, ) + output_bytes = self.translate_string_raw(input.SerializeToString()) + return anki.generic_pb2.String.FromString(output_bytes).val + def format_time_span( self, seconds: Any, @@ -123,31 +133,17 @@ class RustBackend(RustBackendGenerated): ) return self.format_timespan(seconds=seconds, context=context) - def _run_command(self, service: int, method: int, input: Any) -> bytes: - input_bytes = input.SerializeToString() + def _run_command(self, service: int, method: int, input: bytes) -> bytes: try: - return self._backend.command(service, method, input_bytes) + return self._backend.command(service, method, input) except Exception as error: - err_bytes = bytes(error.args[0]) + error_bytes = bytes(error.args[0]) + err = backend_pb2.BackendError() - err.ParseFromString(err_bytes) + err.ParseFromString(error_bytes) raise backend_exception_to_pylib(err) -def translate_string_in( - module_index: int, message_index: int, **kwargs: str | int | float -) -> i18n_pb2.TranslateStringRequest: - args = { - k: i18n_pb2.TranslateArgValue(str=v) - if isinstance(v, str) - else i18n_pb2.TranslateArgValue(number=v) - for k, v in kwargs.items() - } - return i18n_pb2.TranslateStringRequest( - module_index=module_index, message_index=message_index, args=args - ) - - class Translations(GeneratedTranslations): def __init__(self, backend: ref[RustBackend] | None): self.backend = backend diff --git a/pylib/anki/_backend/genbackend.py b/pylib/anki/_backend/genbackend.py index e4ce2f801..a22fd9599 100755 --- a/pylib/anki/_backend/genbackend.py +++ b/pylib/anki/_backend/genbackend.py @@ -51,24 +51,7 @@ LABEL_OPTIONAL = 1 LABEL_REQUIRED = 2 LABEL_REPEATED = 3 -# messages we don't want to unroll in codegen -SKIP_UNROLL_INPUT = { - "TranslateString", - "SetPreferences", - "UpdateDeckConfigs", - "AnswerCard", - "ChangeNotetype", - "CompleteTag", -} -SKIP_UNROLL_OUTPUT = {"GetPreferences"} - -SKIP_DECODE = { - "Graphs", - "GetGraphPreferences", - "GetChangeNotetypeInfo", - "CompleteTag", - "CardStats", -} +RAW_ONLY = {"TranslateString"} def python_type(field): @@ -116,62 +99,59 @@ def fix_snakecase(name): return name -def get_input_args(msg): - fields = sorted(msg.fields, key=lambda x: x.number) +def get_input_args(input_type): + fields = sorted(input_type.fields, key=lambda x: x.number) self_star = ["self"] if len(fields) >= 2: self_star.append("*") return ", ".join(self_star + [f"{f.name}: {python_type(f)}" for f in fields]) -def get_input_assign(msg): - fields = sorted(msg.fields, key=lambda x: x.number) +def get_input_assign(input_type): + fields = sorted(input_type.fields, key=lambda x: x.number) return ", ".join(f"{f.name}={f.name}" for f in fields) def render_method(service_idx, method_idx, method): - input_name = method.input_type.name - if ( - (input_name.endswith("Request") or len(method.input_type.fields) < 2) - and not method.input_type.oneofs - and not method.name in SKIP_UNROLL_INPUT - ): - input_args = get_input_args(method.input_type) - input_assign = get_input_assign(method.input_type) - input_assign_outer = ( - f"input = {fullname(method.input_type.full_name)}({input_assign})\n " - ) - else: - input_args = f"self, input: {fullname(method.input_type.full_name)}" - input_assign_outer = "" name = fix_snakecase(stringcase.snakecase(method.name)) + input_name = method.input_type.name + + if ( + input_name.endswith("Request") or len(method.input_type.fields) < 2 + ) and not method.input_type.oneofs: + input_params = get_input_args(method.input_type) + input_assign_full = f"message = {fullname(method.input_type.full_name)}({get_input_assign(method.input_type)})" + else: + input_params = f"self, message: {fullname(method.input_type.full_name)}" + input_assign_full = "" + if ( len(method.output_type.fields) == 1 and method.output_type.fields[0].type != TYPE_ENUM - and method.name not in SKIP_UNROLL_OUTPUT ): # unwrap single return arg f = method.output_type.fields[0] - single_field = f".{f.name}" return_type = python_type(f) + single_attribute = f".{f.name}" else: - single_field = "" return_type = fullname(method.output_type.full_name) - - if method.name in SKIP_DECODE: - return_type = "bytes" + single_attribute = "" buf = f"""\ - def {name}({input_args}) -> {return_type}: - {input_assign_outer}""" + def {name}_raw(self, message: bytes) -> bytes: + return self._run_command({service_idx}, {method_idx}, message) - if method.name in SKIP_DECODE: - buf += f"""return self._run_command({service_idx}, {method_idx}, input) """ - else: - buf += f"""output = {fullname(method.output_type.full_name)}() - output.ParseFromString(self._run_command({service_idx}, {method_idx}, input)) - return output{single_field} + + if not method.name in RAW_ONLY: + buf += f"""\ + def {name}({input_params}) -> {return_type}: + {input_assign_full} + raw_bytes = self._run_command({service_idx}, {method_idx}, message.SerializeToString()) + output = {fullname(method.output_type.full_name)}() + output.ParseFromString(raw_bytes) + return output{single_attribute} + """ return buf @@ -235,7 +215,7 @@ 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() """ - + from typing import * import anki diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 37642cf56..7eaac67f2 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -804,25 +804,12 @@ class Collection(DeprecatedNamesMixin): return CollectionStats(self) - def card_stats_data(self, card_id: CardId) -> bytes: + def card_stats_data(self, card_id: CardId) -> stats_pb2.CardStatsResponse: return self._backend.card_stats(card_id) def studied_today(self) -> str: return self._backend.studied_today() - def graph_data(self, search: str, days: int) -> bytes: - return self._backend.graphs(search=search, days=days) - - def get_graph_preferences(self) -> bytes: - return self._backend.get_graph_preferences() - - def set_graph_preferences(self, prefs: GraphPreferences) -> None: - self._backend.set_graph_preferences(input=prefs) - - def congrats_info(self) -> bytes: - "Don't use this, it will likely go away in the future." - return self._backend.congrats_info().SerializeToString() - # Undo ########################################################################## diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 1baabbe71..38a4d069c 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -285,7 +285,8 @@ class DeckManager(DeprecatedNamesMixin): return self.col._backend.get_deck_configs_for_update(deck_id) def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges: - return self.col._backend.update_deck_configs(input=input) + op_bytes = self.col._backend.update_deck_configs_raw(input.SerializeToString()) + return OpChanges.FromString(op_bytes) def all_config(self) -> list[DeckConfigDict]: "A list of all deck config." diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 96ca68888..c6afcc876 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -370,7 +370,7 @@ and notes.mid = ? and cards.ord = ?""", def change_notetype_info( self, *, old_notetype_id: NotetypeId, new_notetype_id: NotetypeId - ) -> bytes: + ) -> ChangeNotetypeInfo: return self.col._backend.get_change_notetype_info( old_notetype_id=old_notetype_id, new_notetype_id=new_notetype_id ) @@ -388,7 +388,8 @@ and notes.mid = ? and cards.ord = ?""", field/template count. Each value represents the index in the previous notetype. -1 indicates the original value will be discarded. """ - return self.col._backend.change_notetype(input) + op_bytes = self.col._backend.change_notetype_raw(input.SerializeToString()) + return OpChanges.FromString(op_bytes) # legacy API - used by unit tests and add-ons @@ -414,15 +415,13 @@ and notes.mid = ? and cards.ord = ?""", template_map = self._convert_legacy_map(cmap, len(newModel["tmpls"])) self.col._backend.change_notetype( - ChangeNotetypeRequest( - note_ids=nids, - new_fields=field_map, - new_templates=template_map, - old_notetype_name=notetype["name"], - old_notetype_id=notetype["id"], - new_notetype_id=newModel["id"], - current_schema=self.col.db.scalar("select scm from col"), - ) + note_ids=nids, + new_fields=field_map, + new_templates=template_map, + old_notetype_name=notetype["name"], + old_notetype_id=notetype["id"], + new_notetype_id=newModel["id"], + current_schema=self.col.db.scalar("select scm from col"), ) def _convert_legacy_map( diff --git a/pylib/anki/scheduler/v3.py b/pylib/anki/scheduler/v3.py index 0c959f968..af4fcaed8 100644 --- a/pylib/anki/scheduler/v3.py +++ b/pylib/anki/scheduler/v3.py @@ -86,7 +86,8 @@ class Scheduler(SchedulerBaseWithLegacy): def answer_card(self, input: CardAnswer) -> OpChanges: "Update card to provided state, and remove it from queue." self.reps += 1 - return self.col._backend.answer_card(input=input) + op_bytes = self.col._backend.answer_card_raw(input.SerializeToString()) + return OpChanges.FromString(op_bytes) def state_is_leech(self, new_state: SchedulingState) -> bool: "True if new state marks the card as a leech." diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index 390900ae4..0e894ec75 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -26,6 +26,7 @@ from anki.utils import ids2str # public exports TagTreeNode = tags_pb2.TagTreeNode +CompleteTagRequest = tags_pb2.CompleteTagRequest MARKED_TAG = "marked" @@ -68,11 +69,6 @@ class TagManager(DeprecatedNamesMixin): "Set browser expansion state for tag, registering the tag if missing." return self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed) - def complete_tag(self, input_bytes: bytes) -> bytes: - input = tags_pb2.CompleteTagRequest() - input.ParseFromString(input_bytes) - return self.col._backend.complete_tag(input) - # Bulk addition/removal from specific notes ############################################################# diff --git a/pylib/tests/test_stats.py b/pylib/tests/test_stats.py index dd19d8e01..1fd9b27de 100644 --- a/pylib/tests/test_stats.py +++ b/pylib/tests/test_stats.py @@ -15,14 +15,13 @@ def test_stats(): col.addNote(note) c = note.cards()[0] # card stats - card_stats = CardStats() - card_stats.ParseFromString(col.card_stats_data(c.id)) + card_stats = col.card_stats_data(c.id) assert card_stats.note_id == note.id col.reset() c = col.sched.getCard() col.sched.answerCard(c, 3) col.sched.answerCard(c, 2) - card_stats.ParseFromString(col.card_stats_data(c.id)) + card_stats = col.card_stats_data(c.id) assert len(card_stats.revlog) == 2 diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 57271e163..ea3018a90 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -13,6 +13,7 @@ import time import traceback from dataclasses import dataclass from http import HTTPStatus +from typing import Callable import flask import flask_cors # type: ignore @@ -21,15 +22,14 @@ from waitress.server import create_server import aqt from anki import hooks -from anki.cards import CardId -from anki.collection import GraphPreferences, OpChanges -from anki.decks import UpdateDeckConfigs -from anki.models import NotetypeNames +from anki._vendor import stringcase +from anki.collection import OpChanges +from anki.decks import DeckConfigsForUpdate, UpdateDeckConfigs from anki.scheduler.v3 import NextStates -from anki.utils import dev_mode, from_json_bytes +from anki.utils import dev_mode from aqt.changenotetype import ChangeNotetypeDialog from aqt.deckoptions import DeckOptionsDialog -from aqt.operations.deck import update_deck_configs +from aqt.operations.deck import update_deck_configs as update_deck_configs_op from aqt.qt import * app = flask.Flask(__name__, root_path="/fake") @@ -375,40 +375,20 @@ def _extract_request( return LocalFileRequest(root=aqt.mw.col.media.dir(), path=path) -def graph_data() -> bytes: - args = from_json_bytes(request.data) - return aqt.mw.col.graph_data(search=args["search"], days=args["days"]) - - -def graph_preferences() -> bytes: - return aqt.mw.col.get_graph_preferences() - - -def set_graph_preferences() -> None: - prefs = GraphPreferences() - prefs.ParseFromString(request.data) - aqt.mw.col.set_graph_preferences(prefs) - - def congrats_info() -> bytes: if not aqt.mw.col.sched._is_finished(): aqt.mw.taskman.run_on_main(lambda: aqt.mw.moveToState("review")) - return aqt.mw.col.congrats_info() + return raw_backend_request("congrats_info")() -def i18n_resources() -> bytes: - args = from_json_bytes(request.data) - return aqt.mw.col.i18n_resources(modules=args["modules"]) +def get_deck_configs_for_update() -> bytes: + config_bytes = aqt.mw.col._backend.get_deck_configs_for_update_raw(request.data) + configs = DeckConfigsForUpdate.FromString(config_bytes) + configs.have_addons = aqt.mw.addonManager.dirty + return configs.SerializeToString() -def deck_configs_for_update() -> bytes: - args = from_json_bytes(request.data) - msg = aqt.mw.col.decks.get_deck_configs_for_update(deck_id=args["deckId"]) - msg.have_addons = aqt.mw.addonManager.dirty - return msg.SerializeToString() - - -def update_deck_configs_request() -> bytes: +def update_deck_configs() -> bytes: # the regular change tracking machinery expects to be started on the main # thread and uses a callback on success, so we need to run this op on # main, and return immediately from the web request @@ -421,7 +401,7 @@ def update_deck_configs_request() -> bytes: window.reject() def handle_on_main() -> None: - update_deck_configs(parent=aqt.mw, input=input).success( + update_deck_configs_op(parent=aqt.mw, input=input).success( on_success ).run_in_background() @@ -444,18 +424,6 @@ def set_next_card_states() -> bytes: return b"" -def notetype_names() -> bytes: - msg = NotetypeNames(entries=aqt.mw.col.models.all_names_and_ids()) - return msg.SerializeToString() - - -def change_notetype_info() -> bytes: - args = from_json_bytes(request.data) - return aqt.mw.col.models.change_notetype_info( - old_notetype_id=args["oldNotetypeId"], new_notetype_id=args["newNotetypeId"] - ) - - def change_notetype() -> bytes: data = request.data @@ -468,31 +436,49 @@ def change_notetype() -> bytes: return b"" -def complete_tag() -> bytes: - return aqt.mw.col.tags.complete_tag(request.data) +post_handler_list = [ + congrats_info, + get_deck_configs_for_update, + update_deck_configs, + next_card_states, + set_next_card_states, + change_notetype, +] -def card_stats() -> bytes: - args = from_json_bytes(request.data) - return aqt.mw.col.card_stats_data(CardId(args["cardId"])) +exposed_backend_list = [ + # I18nService + "i18n_resources", + # NotesService + "get_note", + # NotetypesService + "get_notetype_names", + "get_change_notetype_info", + # StatsService + "card_stats", + "graphs", + "get_graph_preferences", + "set_graph_preferences", + # TagsService + "complete_tag", +] -# these require a collection +def raw_backend_request(endpoint: str) -> Callable[[], bytes]: + # check for key at startup + from anki._backend import RustBackend + + assert hasattr(RustBackend, f"{endpoint}_raw") + + return lambda: getattr(aqt.mw.col._backend, f"{endpoint}_raw")(request.data) + + +# all methods in here require a collection post_handlers = { - "graphData": graph_data, - "graphPreferences": graph_preferences, - "setGraphPreferences": set_graph_preferences, - "deckConfigsForUpdate": deck_configs_for_update, - "updateDeckConfigs": update_deck_configs_request, - "nextCardStates": next_card_states, - "setNextCardStates": set_next_card_states, - "changeNotetypeInfo": change_notetype_info, - "notetypeNames": notetype_names, - "changeNotetype": change_notetype, - "i18nResources": i18n_resources, - "congratsInfo": congrats_info, - "completeTag": complete_tag, - "cardStats": card_stats, + stringcase.camelcase(handler.__name__): handler for handler in post_handler_list +} | { + stringcase.camelcase(handler): raw_backend_request(handler) + for handler in exposed_backend_list } diff --git a/ts/card-info/CardInfo.svelte b/ts/card-info/CardInfo.svelte index 2c9d05497..507bb8ee6 100644 --- a/ts/card-info/CardInfo.svelte +++ b/ts/card-info/CardInfo.svelte @@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->