diff --git a/proto/backend.proto b/proto/backend.proto index 53fb173d5..fe56065f6 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -158,6 +158,27 @@ service BackendService { rpc SyncMedia (SyncMediaIn) returns (Empty); rpc AbortMediaSync (Empty) returns (Empty); rpc BeforeUpload (Empty) returns (Empty); + + // translation/messages + + rpc TranslateString (TranslateStringIn) returns (String); + rpc FormatTimespan (FormatTimespanIn) returns (String); + + // tags + + rpc RegisterTags (RegisterTagsIn) returns (Bool); + rpc AllTags (Empty) returns (AllTagsOut); + rpc GetChangedTags (Int32) returns (GetChangedTagsOut); + + // config/preferences + + rpc GetConfigJson (String) returns (Json); + rpc SetConfigJson (SetConfigJsonIn) returns (Empty); + rpc RemoveConfig (String) returns (Empty); + rpc SetAllConfig (Json) returns (Empty); + rpc GetAllConfig (Empty) returns (Json); + rpc GetPreferences (Empty) returns (Preferences); + rpc SetPreferences (Preferences) returns (Empty); } // Protobuf stored in .anki2 files @@ -398,49 +419,9 @@ message I18nBackendInit { string locale_folder_path = 5; } -// Legacy enum - needs migrating to RPC above +// Errors /////////////////////////////////////////////////////////// -message BackendInput { - oneof value { - TranslateStringIn translate_string = 30; - FormatTimeSpanIn format_time_span = 31; - - RegisterTagsIn register_tags = 48; - Empty all_tags = 50; - int32 get_changed_tags = 51; - - string get_config_json = 52; - SetConfigJson set_config_json = 53; - bytes set_all_config = 54; - Empty get_all_config = 55; - - Empty get_preferences = 84; - Preferences set_preferences = 85; - } -} - -message BackendOutput { - oneof value { - string translate_string = 30; - string format_time_span = 31; - - bool register_tags = 48; - AllTagsOut all_tags = 50; - GetChangedTagsOut get_changed_tags = 51; - - bytes get_config_json = 52; - Empty set_config_json = 53; - Empty set_all_config = 54; - bytes get_all_config = 55; - - Preferences get_preferences = 84; - Empty set_preferences = 85; - - BackendError error = 2047; - } -} - message BackendError { // localized error description suitable for displaying to the user string localized = 1; @@ -504,6 +485,10 @@ message MediaSyncUploadProgress { uint32 deletions = 2; } +// Messages +/////////////////////////////////////////////////////////// + + message SchedTimingTodayOut { uint32 days_elapsed = 1; int64 next_day_at = 2; @@ -632,7 +617,7 @@ message TranslateArgValue { } } -message FormatTimeSpanIn { +message FormatTimespanIn { enum Context { PRECISE = 0; ANSWER_BUTTONS = 1; @@ -736,12 +721,9 @@ message GetChangedTagsOut { repeated string tags = 1; } -message SetConfigJson { +message SetConfigJsonIn { string key = 1; - oneof op { - bytes val = 2; - Empty remove = 3; - } + bytes val = 2; } enum StockNoteType { diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 2c37f805a..2a1b1da33 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -26,7 +26,13 @@ from anki.lang import _ from anki.media import MediaManager, media_paths_from_col_path from anki.models import ModelManager from anki.notes import Note -from anki.rsbackend import TR, DBError, RustBackend, pb +from anki.rsbackend import ( + TR, + DBError, + FormatTimeSpanContext, + RustBackend, + pb, +) from anki.sched import Scheduler as V1Scheduler from anki.schedv2 import Scheduler as V2Scheduler from anki.tags import TagManager @@ -65,13 +71,23 @@ class Collection: n = os.path.splitext(os.path.basename(self.path))[0] return n - def tr(self, key: TR, **kwargs: Union[str, int, float]) -> str: - return self.backend.translate(key, **kwargs) - def weakref(self) -> Collection: "Shortcut to create a weak reference that doesn't break code completion." return weakref.proxy(self) + # I18n/messages + ########################################################################## + + def tr(self, key: TR, **kwargs: Union[str, int, float]) -> str: + return self.backend.translate(key, **kwargs) + + def format_timespan( + self, + seconds: float, + context: FormatTimeSpanContext = FormatTimeSpanContext.INTERVALS, + ) -> str: + return self.backend.format_timespan(seconds, context) + # Scheduler ########################################################################## diff --git a/pylib/anki/config.py b/pylib/anki/config.py index 74afb0d80..0c853c629 100644 --- a/pylib/anki/config.py +++ b/pylib/anki/config.py @@ -23,6 +23,7 @@ import weakref from typing import Any import anki +from anki.rsbackend import NotFoundError, from_json_bytes, to_json_bytes class ConfigManager: @@ -30,10 +31,13 @@ class ConfigManager: self.col = col.weakref() def get_immutable(self, key: str) -> Any: - return self.col.backend.get_config_json(key) + try: + return from_json_bytes(self.col.backend.get_config_json(key)) + except NotFoundError: + raise KeyError def set(self, key: str, val: Any) -> None: - self.col.backend.set_config_json(key, val) + self.col.backend.set_config_json(key, to_json_bytes(val)) def remove(self, key: str) -> None: self.col.backend.remove_config(key) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 987c478e6..06f11024b 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -153,7 +153,7 @@ def proto_exception_to_native(err: pb.BackendError) -> Exception: MediaSyncProgress = pb.MediaSyncProgress -FormatTimeSpanContext = pb.FormatTimeSpanIn.Context +FormatTimeSpanContext = pb.FormatTimespanIn.Context class ProgressKind(enum.Enum): @@ -203,35 +203,6 @@ class RustBackend: self._backend = ankirspy.open_backend(init_msg.SerializeToString()) self._backend.set_progress_callback(_on_progress) - def _run_command( - self, input: pb.BackendInput, release_gil: bool = False - ) -> pb.BackendOutput: - input_bytes = input.SerializeToString() - output_bytes = self._backend.command(input_bytes, release_gil) - output = pb.BackendOutput() - output.ParseFromString(output_bytes) - kind = output.WhichOneof("value") - if kind == "error": - raise proto_exception_to_native(output.error) - else: - return output - - def translate(self, key: TR, **kwargs: Union[str, int, float]) -> str: - return self._run_command( - pb.BackendInput(translate_string=translate_string_in(key, **kwargs)) - ).translate_string - - def format_time_span( - self, - seconds: float, - context: FormatTimeSpanContext = FormatTimeSpanContext.INTERVALS, - ) -> str: - return self._run_command( - pb.BackendInput( - format_time_span=pb.FormatTimeSpanIn(seconds=seconds, context=context) - ) - ).format_time_span - def db_query( self, sql: str, args: Sequence[ValueForDB], first_row_only: bool ) -> List[DBRow]: @@ -254,76 +225,23 @@ class RustBackend: def _db_command(self, input: Dict[str, Any]) -> Any: return orjson.loads(self._backend.db_command(orjson.dumps(input))) - def all_tags(self) -> Iterable[TagUsnTuple]: - return self._run_command(pb.BackendInput(all_tags=pb.Empty())).all_tags.tags + def translate(self, key: TR, **kwargs: Union[str, int, float]) -> str: + return self.translate_string(translate_string_in(key, **kwargs)) - def register_tags(self, tags: str, usn: Optional[int], clear_first: bool) -> bool: - if usn is None: - preserve_usn = False - usn_ = 0 - else: - usn_ = usn - preserve_usn = True - - return self._run_command( - pb.BackendInput( - register_tags=pb.RegisterTagsIn( - tags=tags, - usn=usn_, - preserve_usn=preserve_usn, - clear_first=clear_first, - ) - ) - ).register_tags - - def get_changed_tags(self, usn: int) -> List[str]: - return list( - self._run_command( - pb.BackendInput(get_changed_tags=usn) - ).get_changed_tags.tags + def format_time_span( + self, + seconds: float, + context: FormatTimeSpanContext = FormatTimeSpanContext.INTERVALS, + ) -> str: + print( + "please use col.format_timespan() instead of col.backend.format_time_span()" ) + return self.format_timespan(seconds, context) - def get_config_json(self, key: str) -> Any: - b = self._run_command(pb.BackendInput(get_config_json=key)).get_config_json - if b == b"": - raise KeyError - return orjson.loads(b) - - def set_config_json(self, key: str, val: Any): - self._run_command( - pb.BackendInput( - set_config_json=pb.SetConfigJson(key=key, val=orjson.dumps(val)) - ) - ) - - def remove_config(self, key: str): - self._run_command( - pb.BackendInput( - set_config_json=pb.SetConfigJson(key=key, remove=pb.Empty()) - ) - ) - - def get_all_config(self) -> Dict[str, Any]: - jstr = self._run_command( - pb.BackendInput(get_all_config=pb.Empty()) - ).get_all_config - return orjson.loads(jstr) - - def set_all_config(self, conf: Dict[str, Any]): - self._run_command(pb.BackendInput(set_all_config=orjson.dumps(conf))) - - def get_preferences(self) -> pb.Preferences: - return self._run_command( - pb.BackendInput(get_preferences=pb.Empty()) - ).get_preferences - - def set_preferences(self, prefs: pb.Preferences) -> None: - self._run_command(pb.BackendInput(set_preferences=prefs)) - - def _run_command2(self, method: int, input: Any) -> bytes: + def _run_command(self, method: int, input: Any) -> bytes: input_bytes = input.SerializeToString() try: - return self._backend.command2(method, input_bytes) + return self._backend.command(method, input_bytes) except Exception as e: err_bytes = bytes(e.args[0]) err = pb.BackendError() @@ -338,7 +256,7 @@ class RustBackend: def extract_av_tags(self, text: str, question_side: bool) -> pb.ExtractAVTagsOut: input = pb.ExtractAVTagsIn(text=text, question_side=question_side) output = pb.ExtractAVTagsOut() - output.ParseFromString(self._run_command2(1, input)) + output.ParseFromString(self._run_command(1, input)) return output def extract_latex( @@ -346,19 +264,19 @@ class RustBackend: ) -> pb.ExtractLatexOut: input = pb.ExtractLatexIn(text=text, svg=svg, expand_clozes=expand_clozes) output = pb.ExtractLatexOut() - output.ParseFromString(self._run_command2(2, input)) + output.ParseFromString(self._run_command(2, input)) return output def get_empty_cards(self) -> pb.EmptyCardsReport: input = pb.Empty() output = pb.EmptyCardsReport() - output.ParseFromString(self._run_command2(3, input)) + output.ParseFromString(self._run_command(3, input)) return output def render_existing_card(self, card_id: int, browser: bool) -> pb.RenderCardOut: input = pb.RenderExistingCardIn(card_id=card_id, browser=browser) output = pb.RenderCardOut() - output.ParseFromString(self._run_command2(4, input)) + output.ParseFromString(self._run_command(4, input)) return output def render_uncommitted_card( @@ -368,25 +286,25 @@ class RustBackend: note=note, card_ord=card_ord, template=template, fill_empty=fill_empty ) output = pb.RenderCardOut() - output.ParseFromString(self._run_command2(5, input)) + output.ParseFromString(self._run_command(5, input)) return output def strip_av_tags(self, val: str) -> str: input = pb.String(val=val) output = pb.String() - output.ParseFromString(self._run_command2(6, input)) + output.ParseFromString(self._run_command(6, input)) return output.val def search_cards(self, search: str, order: pb.SortOrder) -> Sequence[int]: input = pb.SearchCardsIn(search=search, order=order) output = pb.SearchCardsOut() - output.ParseFromString(self._run_command2(7, input)) + output.ParseFromString(self._run_command(7, input)) return output.card_ids def search_notes(self, search: str) -> Sequence[int]: input = pb.SearchNotesIn(search=search) output = pb.SearchNotesOut() - output.ParseFromString(self._run_command2(8, input)) + output.ParseFromString(self._run_command(8, input)) return output.note_ids def find_and_replace( @@ -407,67 +325,67 @@ class RustBackend: field_name=field_name, ) output = pb.UInt32() - output.ParseFromString(self._run_command2(9, input)) + output.ParseFromString(self._run_command(9, input)) return output.val def local_minutes_west(self, val: int) -> int: input = pb.Int64(val=val) output = pb.Int32() - output.ParseFromString(self._run_command2(10, input)) + output.ParseFromString(self._run_command(10, input)) return output.val def set_local_minutes_west(self, val: int) -> pb.Empty: input = pb.Int32(val=val) output = pb.Empty() - output.ParseFromString(self._run_command2(11, input)) + output.ParseFromString(self._run_command(11, input)) return output def sched_timing_today(self) -> pb.SchedTimingTodayOut: input = pb.Empty() output = pb.SchedTimingTodayOut() - output.ParseFromString(self._run_command2(12, input)) + output.ParseFromString(self._run_command(12, input)) return output def studied_today(self, cards: int, seconds: float) -> str: input = pb.StudiedTodayIn(cards=cards, seconds=seconds) output = pb.String() - output.ParseFromString(self._run_command2(13, input)) + output.ParseFromString(self._run_command(13, input)) return output.val def congrats_learn_message(self, next_due: float, remaining: int) -> str: input = pb.CongratsLearnMessageIn(next_due=next_due, remaining=remaining) output = pb.String() - output.ParseFromString(self._run_command2(14, input)) + output.ParseFromString(self._run_command(14, input)) return output.val def check_media(self) -> pb.CheckMediaOut: input = pb.Empty() output = pb.CheckMediaOut() - output.ParseFromString(self._run_command2(15, input)) + output.ParseFromString(self._run_command(15, input)) return output def trash_media_files(self, fnames: Sequence[str]) -> pb.Empty: input = pb.TrashMediaFilesIn(fnames=fnames) output = pb.Empty() - output.ParseFromString(self._run_command2(16, input)) + output.ParseFromString(self._run_command(16, input)) return output def add_media_file(self, desired_name: str, data: bytes) -> str: input = pb.AddMediaFileIn(desired_name=desired_name, data=data) output = pb.String() - output.ParseFromString(self._run_command2(17, input)) + output.ParseFromString(self._run_command(17, input)) return output.val def empty_trash(self) -> pb.Empty: input = pb.Empty() output = pb.Empty() - output.ParseFromString(self._run_command2(18, input)) + output.ParseFromString(self._run_command(18, input)) return output def restore_trash(self) -> pb.Empty: input = pb.Empty() output = pb.Empty() - output.ParseFromString(self._run_command2(19, input)) + output.ParseFromString(self._run_command(19, input)) return output def add_or_update_deck_legacy( @@ -477,37 +395,37 @@ class RustBackend: deck=deck, preserve_usn_and_mtime=preserve_usn_and_mtime ) output = pb.DeckID() - output.ParseFromString(self._run_command2(20, input)) + output.ParseFromString(self._run_command(20, input)) return output.did def deck_tree(self, include_counts: bool, top_deck_id: int) -> pb.DeckTreeNode: input = pb.DeckTreeIn(include_counts=include_counts, top_deck_id=top_deck_id) output = pb.DeckTreeNode() - output.ParseFromString(self._run_command2(21, input)) + output.ParseFromString(self._run_command(21, input)) return output def deck_tree_legacy(self) -> bytes: input = pb.Empty() output = pb.Json() - output.ParseFromString(self._run_command2(22, input)) + output.ParseFromString(self._run_command(22, input)) return output.json def get_all_decks_legacy(self) -> bytes: input = pb.Empty() output = pb.Json() - output.ParseFromString(self._run_command2(23, input)) + output.ParseFromString(self._run_command(23, input)) return output.json def get_deck_id_by_name(self, val: str) -> int: input = pb.String(val=val) output = pb.DeckID() - output.ParseFromString(self._run_command2(24, input)) + output.ParseFromString(self._run_command(24, input)) return output.did def get_deck_legacy(self, did: int) -> bytes: input = pb.DeckID(did=did) output = pb.Json() - output.ParseFromString(self._run_command2(25, input)) + output.ParseFromString(self._run_command(25, input)) return output.json def get_deck_names( @@ -517,19 +435,19 @@ class RustBackend: skip_empty_default=skip_empty_default, include_filtered=include_filtered ) output = pb.DeckNames() - output.ParseFromString(self._run_command2(26, input)) + output.ParseFromString(self._run_command(26, input)) return output.entries def new_deck_legacy(self, val: bool) -> bytes: input = pb.Bool(val=val) output = pb.Json() - output.ParseFromString(self._run_command2(27, input)) + output.ParseFromString(self._run_command(27, input)) return output.json def remove_deck(self, did: int) -> pb.Empty: input = pb.DeckID(did=did) output = pb.Empty() - output.ParseFromString(self._run_command2(28, input)) + output.ParseFromString(self._run_command(28, input)) return output def add_or_update_deck_config_legacy( @@ -539,76 +457,76 @@ class RustBackend: config=config, preserve_usn_and_mtime=preserve_usn_and_mtime ) output = pb.DeckConfigID() - output.ParseFromString(self._run_command2(29, input)) + output.ParseFromString(self._run_command(29, input)) return output.dcid def all_deck_config_legacy(self) -> bytes: input = pb.Empty() output = pb.Json() - output.ParseFromString(self._run_command2(30, input)) + output.ParseFromString(self._run_command(30, input)) return output.json def get_deck_config_legacy(self, dcid: int) -> bytes: input = pb.DeckConfigID(dcid=dcid) output = pb.Json() - output.ParseFromString(self._run_command2(31, input)) + output.ParseFromString(self._run_command(31, input)) return output.json def new_deck_config_legacy(self) -> bytes: input = pb.Empty() output = pb.Json() - output.ParseFromString(self._run_command2(32, input)) + output.ParseFromString(self._run_command(32, input)) return output.json def remove_deck_config(self, dcid: int) -> pb.Empty: input = pb.DeckConfigID(dcid=dcid) output = pb.Empty() - output.ParseFromString(self._run_command2(33, input)) + output.ParseFromString(self._run_command(33, input)) return output def get_card(self, cid: int) -> pb.Card: input = pb.CardID(cid=cid) output = pb.Card() - output.ParseFromString(self._run_command2(34, input)) + output.ParseFromString(self._run_command(34, input)) return output def update_card(self, input: pb.Card) -> pb.Empty: output = pb.Empty() - output.ParseFromString(self._run_command2(35, input)) + output.ParseFromString(self._run_command(35, input)) return output def add_card(self, input: pb.Card) -> int: output = pb.CardID() - output.ParseFromString(self._run_command2(36, input)) + output.ParseFromString(self._run_command(36, input)) return output.cid def new_note(self, ntid: int) -> pb.Note: input = pb.NoteTypeID(ntid=ntid) output = pb.Note() - output.ParseFromString(self._run_command2(37, input)) + output.ParseFromString(self._run_command(37, input)) return output def add_note(self, note: pb.Note, deck_id: int) -> int: input = pb.AddNoteIn(note=note, deck_id=deck_id) output = pb.NoteID() - output.ParseFromString(self._run_command2(38, input)) + output.ParseFromString(self._run_command(38, input)) return output.nid def update_note(self, input: pb.Note) -> pb.Empty: output = pb.Empty() - output.ParseFromString(self._run_command2(39, input)) + output.ParseFromString(self._run_command(39, input)) return output def get_note(self, nid: int) -> pb.Note: input = pb.NoteID(nid=nid) output = pb.Note() - output.ParseFromString(self._run_command2(40, input)) + output.ParseFromString(self._run_command(40, input)) return output def add_note_tags(self, nids: Sequence[int], tags: str) -> int: input = pb.AddNoteTagsIn(nids=nids, tags=tags) output = pb.UInt32() - output.ParseFromString(self._run_command2(41, input)) + output.ParseFromString(self._run_command(41, input)) return output.val def update_note_tags( @@ -618,12 +536,12 @@ class RustBackend: nids=nids, tags=tags, replacement=replacement, regex=regex ) output = pb.UInt32() - output.ParseFromString(self._run_command2(42, input)) + output.ParseFromString(self._run_command(42, input)) return output.val def cloze_numbers_in_note(self, input: pb.Note) -> Sequence[int]: output = pb.ClozeNumbersInNoteOut() - output.ParseFromString(self._run_command2(43, input)) + output.ParseFromString(self._run_command(43, input)) return output.numbers def after_note_updates( @@ -635,13 +553,13 @@ class RustBackend: generate_cards=generate_cards, ) output = pb.Empty() - output.ParseFromString(self._run_command2(44, input)) + output.ParseFromString(self._run_command(44, input)) return output def field_names_for_notes(self, nids: Sequence[int]) -> Sequence[str]: input = pb.FieldNamesForNotesIn(nids=nids) output = pb.FieldNamesForNotesOut() - output.ParseFromString(self._run_command2(45, input)) + output.ParseFromString(self._run_command(45, input)) return output.fields def add_or_update_notetype(self, json: bytes, preserve_usn_and_mtime: bool) -> int: @@ -649,43 +567,43 @@ class RustBackend: json=json, preserve_usn_and_mtime=preserve_usn_and_mtime ) output = pb.NoteTypeID() - output.ParseFromString(self._run_command2(46, input)) + output.ParseFromString(self._run_command(46, input)) return output.ntid def get_stock_notetype_legacy(self, kind: pb.StockNoteType) -> bytes: input = pb.GetStockNotetypeIn(kind=kind) output = pb.Json() - output.ParseFromString(self._run_command2(47, input)) + output.ParseFromString(self._run_command(47, input)) return output.json def get_notetype_legacy(self, ntid: int) -> bytes: input = pb.NoteTypeID(ntid=ntid) output = pb.Json() - output.ParseFromString(self._run_command2(48, input)) + output.ParseFromString(self._run_command(48, input)) return output.json def get_notetype_names(self) -> Sequence[pb.NoteTypeNameID]: input = pb.Empty() output = pb.NoteTypeNames() - output.ParseFromString(self._run_command2(49, input)) + output.ParseFromString(self._run_command(49, input)) return output.entries def get_notetype_names_and_counts(self) -> Sequence[pb.NoteTypeNameIDUseCount]: input = pb.Empty() output = pb.NoteTypeUseCounts() - output.ParseFromString(self._run_command2(50, input)) + output.ParseFromString(self._run_command(50, input)) return output.entries def get_notetype_id_by_name(self, val: str) -> int: input = pb.String(val=val) output = pb.NoteTypeID() - output.ParseFromString(self._run_command2(51, input)) + output.ParseFromString(self._run_command(51, input)) return output.ntid def remove_notetype(self, ntid: int) -> pb.Empty: input = pb.NoteTypeID(ntid=ntid) output = pb.Empty() - output.ParseFromString(self._run_command2(52, input)) + output.ParseFromString(self._run_command(52, input)) return output def open_collection( @@ -702,37 +620,114 @@ class RustBackend: log_path=log_path, ) output = pb.Empty() - output.ParseFromString(self._run_command2(53, input)) + output.ParseFromString(self._run_command(53, input)) return output def close_collection(self, downgrade_to_schema11: bool) -> pb.Empty: input = pb.CloseCollectionIn(downgrade_to_schema11=downgrade_to_schema11) output = pb.Empty() - output.ParseFromString(self._run_command2(54, input)) + output.ParseFromString(self._run_command(54, input)) return output def check_database(self) -> Sequence[str]: input = pb.Empty() output = pb.CheckDatabaseOut() - output.ParseFromString(self._run_command2(55, input)) + output.ParseFromString(self._run_command(55, input)) return output.problems def sync_media(self, hkey: str, endpoint: str) -> pb.Empty: input = pb.SyncMediaIn(hkey=hkey, endpoint=endpoint) output = pb.Empty() - output.ParseFromString(self._run_command2(56, input)) + output.ParseFromString(self._run_command(56, input)) return output def abort_media_sync(self) -> pb.Empty: input = pb.Empty() output = pb.Empty() - output.ParseFromString(self._run_command2(57, input)) + output.ParseFromString(self._run_command(57, input)) return output def before_upload(self) -> pb.Empty: input = pb.Empty() output = pb.Empty() - output.ParseFromString(self._run_command2(58, input)) + output.ParseFromString(self._run_command(58, input)) + return output + + def translate_string(self, input: pb.TranslateStringIn) -> str: + output = pb.String() + output.ParseFromString(self._run_command(59, input)) + return output.val + + def format_timespan( + self, seconds: float, context: pb.FormatTimespanIn.Context + ) -> str: + input = pb.FormatTimespanIn(seconds=seconds, context=context) + output = pb.String() + output.ParseFromString(self._run_command(60, input)) + return output.val + + def register_tags( + self, tags: str, preserve_usn: bool, usn: int, clear_first: bool + ) -> bool: + input = pb.RegisterTagsIn( + tags=tags, preserve_usn=preserve_usn, usn=usn, clear_first=clear_first + ) + output = pb.Bool() + output.ParseFromString(self._run_command(61, input)) + return output.val + + def all_tags(self) -> Sequence[pb.TagUsnTuple]: + input = pb.Empty() + output = pb.AllTagsOut() + output.ParseFromString(self._run_command(62, input)) + return output.tags + + def get_changed_tags(self, val: int) -> Sequence[str]: + input = pb.Int32(val=val) + output = pb.GetChangedTagsOut() + output.ParseFromString(self._run_command(63, input)) + return output.tags + + def get_config_json(self, val: str) -> bytes: + input = pb.String(val=val) + output = pb.Json() + output.ParseFromString(self._run_command(64, input)) + return output.json + + def set_config_json(self, key: str, val: bytes) -> pb.Empty: + input = pb.SetConfigJsonIn(key=key, val=val) + output = pb.Empty() + output.ParseFromString(self._run_command(65, input)) + return output + + def remove_config(self, val: str) -> pb.Empty: + input = pb.String(val=val) + output = pb.Empty() + output.ParseFromString(self._run_command(66, input)) + return output + + def set_all_config(self, json: bytes) -> pb.Empty: + input = pb.Json(json=json) + output = pb.Empty() + output.ParseFromString(self._run_command(67, input)) + return output + + def get_all_config(self) -> bytes: + input = pb.Empty() + output = pb.Json() + output.ParseFromString(self._run_command(68, input)) + return output.json + + def get_preferences(self) -> pb.CollectionSchedulingSettings: + input = pb.Empty() + output = pb.Preferences() + output.ParseFromString(self._run_command(69, input)) + return output.sched + + def set_preferences(self, sched: pb.CollectionSchedulingSettings) -> pb.Empty: + input = pb.Preferences(sched=sched) + output = pb.Empty() + output.ParseFromString(self._run_command(70, input)) return output # @@AUTOGEN@@ diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 6c4c216d0..63ce56e13 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -1369,9 +1369,7 @@ To study outside of the normal schedule, click the Custom Study button below.""" ivl_secs = self.nextIvl(card, ease) if not ivl_secs: return _("(end)") - s = self.col.backend.format_time_span( - ivl_secs, FormatTimeSpanContext.ANSWER_BUTTONS - ) + s = self.col.format_timespan(ivl_secs, FormatTimeSpanContext.ANSWER_BUTTONS) if ivl_secs < self.col.conf["collapseTime"]: s = "<" + s return s diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 2fc3363a0..d6e330854 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -52,9 +52,7 @@ class CardStats: if next: self.addLine(self.col.tr(TR.STATISTICS_DUE_DATE), next) if c.queue == QUEUE_TYPE_REV: - self.addLine( - _("Interval"), self.col.backend.format_time_span(c.ivl * 86400) - ) + self.addLine(_("Interval"), self.col.format_timespan(c.ivl * 86400)) self.addLine(_("Ease"), "%d%%" % (c.factor / 10.0)) self.addLine(_("Reviews"), "%d" % c.reps) self.addLine(_("Lapses"), "%d" % c.lapses) @@ -86,9 +84,7 @@ class CardStats: return time.strftime("%Y-%m-%d", time.localtime(tm)) def time(self, tm: float) -> str: - return self.col.backend.format_time_span( - tm, context=FormatTimeSpanContext.PRECISE - ) + return self.col.format_timespan(tm, context=FormatTimeSpanContext.PRECISE) # Collection stats @@ -645,12 +641,8 @@ group by day order by day)""" ), ) i: List[str] = [] - self._line( - i, _("Average interval"), self.col.backend.format_time_span(avg * 86400) - ) - self._line( - i, _("Longest interval"), self.col.backend.format_time_span(max_ * 86400) - ) + self._line(i, _("Average interval"), self.col.format_timespan(avg * 86400)) + self._line(i, _("Longest interval"), self.col.format_timespan(max_ * 86400)) return txt + self._lineTbl(i) def _ivls(self) -> Tuple[List[Any], int]: diff --git a/pylib/anki/sync.py b/pylib/anki/sync.py index 8182d8468..8d925802e 100644 --- a/pylib/anki/sync.py +++ b/pylib/anki/sync.py @@ -19,6 +19,8 @@ from . import hooks from .httpclient import HttpClient # add-on compat +from .rsbackend import from_json_bytes, to_json_bytes + AnkiRequestsClient = HttpClient @@ -402,7 +404,7 @@ from notes where %s""" ########################################################################## def getTags(self) -> List: - return self.col.backend.get_changed_tags(self.maxUsn) + return list(self.col.backend.get_changed_tags(self.maxUsn)) def mergeTags(self, tags) -> None: self.col.tags.register(tags, usn=self.maxUsn) @@ -448,10 +450,10 @@ from notes where %s""" ########################################################################## def getConf(self) -> Dict[str, Any]: - return self.col.backend.get_all_config() + return from_json_bytes(self.col.backend.get_all_config()) def mergeConf(self, conf: Dict[str, Any]) -> None: - self.col.backend.set_all_config(conf) + self.col.backend.set_all_config(to_json_bytes(conf)) # HTTP syncing tools diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index 7e27b4f81..c6aa7d910 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -36,7 +36,16 @@ class TagManager: def register( self, tags: Collection[str], usn: Optional[int] = None, clear=False ) -> None: - self.col.backend.register_tags(" ".join(tags), usn, clear) + if usn is None: + preserve_usn = False + usn_ = 0 + else: + usn_ = usn + preserve_usn = True + + self.col.backend.register_tags( + tags=" ".join(tags), preserve_usn=preserve_usn, usn=usn_, clear_first=clear + ) def registerNotes(self, nids: Optional[List[int]] = None) -> None: "Add any missing tags from notes to the tags list." diff --git a/pylib/tools/genbackend.py b/pylib/tools/genbackend.py index 5c4dc7cb8..862595966 100755 --- a/pylib/tools/genbackend.py +++ b/pylib/tools/genbackend.py @@ -29,6 +29,9 @@ LABEL_OPTIONAL = 1 LABEL_REQUIRED = 2 LABEL_REPEATED = 3 +# messages we don't want to unroll in codegen +SKIP_UNROLL_INPUT = {"TranslateString"} + def python_type(field): type = python_type_inner(field) @@ -50,13 +53,20 @@ def python_type_inner(field): elif type == TYPE_BYTES: return "bytes" elif type == TYPE_MESSAGE: - return "pb." + field.message_type.name + return fullname(field.message_type.full_name) elif type == TYPE_ENUM: - return "pb." + field.enum_type.name + return fullname(field.enum_type.full_name) else: raise Exception(f"unknown type: {type}") +def fullname(fullname): + if "FluentString" in fullname: + return fullname.replace("backend_proto", "anki.fluent_pb2") + else: + return fullname.replace("backend_proto", "pb") + + # get_deck_i_d -> get_deck_id etc def fix_snakecase(name): for fix in "a_v", "i_d": @@ -80,14 +90,18 @@ def get_input_assign(msg): def render_method(method, idx): input_name = method.input_type.name - if input_name.endswith("In") or len(method.input_type.fields) < 2: + if ( + (input_name.endswith("In") 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 = pb.{method.input_type.name}({input_assign})\n " + f"input = {fullname(method.input_type.full_name)}({input_assign})\n " ) else: - input_args = f"self, input: pb.{input_name}" + input_args = f"self, input: {fullname(method.input_type.full_name)}" input_assign_outer = "" name = fix_snakecase(stringcase.snakecase(method.name)) if len(method.output_type.fields) == 1: @@ -101,7 +115,7 @@ def render_method(method, idx): return f"""\ def {name}({input_args}) -> {return_type}: {input_assign_outer}output = pb.{method.output_type.name}() - output.ParseFromString(self._run_command2({idx+1}, input)) + output.ParseFromString(self._run_command({idx+1}, input)) return output{single_field} """ diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index f10e59d7e..3ee8623f6 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -323,7 +323,7 @@ class DataModel(QAbstractTableModel): return _("(new)") elif c.type == CARD_TYPE_LRN: return _("(learning)") - return self.col.backend.format_time_span(c.ivl * 86400) + return self.col.format_timespan(c.ivl * 86400) elif type == "cardEase": if c.type == CARD_TYPE_NEW: return _("(new)") @@ -1491,7 +1491,7 @@ border: 1px solid #000; padding: 3px; '>%s""" s += ("%s" * 2) % ( "%d%%" % (factor / 10) if factor else "", - self.col.backend.format_time_span(taken), + self.col.format_timespan(taken), ) + "" s += "" if cnt < self.card.reps: diff --git a/qt/aqt/legacy.py b/qt/aqt/legacy.py index 828025a58..96d458079 100644 --- a/qt/aqt/legacy.py +++ b/qt/aqt/legacy.py @@ -40,7 +40,7 @@ def stripSounds(text) -> str: def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99): print("fmtTimeSpan() has become col.backend.format_time_span()") - return aqt.mw.col.backend.format_time_span(time) + return aqt.mw.col.format_timespan(time) def install_pylib_legacy() -> None: diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 2fb769b26..29705c034 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -41,7 +41,7 @@ use crate::{ use fluent::FluentValue; use futures::future::{AbortHandle, Abortable}; use log::error; -use pb::{backend_input::Value, BackendService}; +use pb::BackendService; use prost::Message; use serde_json::Value as JsonValue; use std::collections::{HashMap, HashSet}; @@ -99,13 +99,6 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError { } } -// Convert an Anki error to a protobuf output. -impl std::convert::From for pb::backend_output::Value { - fn from(err: pb::BackendError) -> Self { - pb::backend_output::Value::Error(err) - } -} - impl std::convert::From for i32 { fn from(e: NetworkErrorKind) -> Self { use pb::network_error::NetworkErrorKind as V; @@ -947,6 +940,133 @@ impl BackendService for Backend { fn before_upload(&mut self, _input: Empty) -> BackendResult { self.with_col(|col| col.before_upload().map(Into::into)) } + + // i18n/messages + //------------------------------------------------------------------- + + fn translate_string(&mut self, input: pb::TranslateStringIn) -> BackendResult { + let key = match pb::FluentString::from_i32(input.key) { + Some(key) => key, + None => return Ok("invalid key".to_string().into()), + }; + + let map = input + .args + .iter() + .map(|(k, v)| (k.as_str(), translate_arg_to_fluent_val(&v))) + .collect(); + + Ok(self.i18n.trn(key, map).into()) + } + + fn format_timespan(&mut self, input: pb::FormatTimespanIn) -> BackendResult { + let context = match pb::format_timespan_in::Context::from_i32(input.context) { + Some(context) => context, + None => return Ok("".to_string().into()), + }; + Ok(match context { + pb::format_timespan_in::Context::Precise => time_span(input.seconds, &self.i18n, true), + pb::format_timespan_in::Context::Intervals => { + time_span(input.seconds, &self.i18n, false) + } + pb::format_timespan_in::Context::AnswerButtons => { + answer_button_time(input.seconds, &self.i18n) + } + } + .into()) + } + + // tags + //------------------------------------------------------------------- + + fn all_tags(&mut self, _input: Empty) -> BackendResult { + let tags = self.with_col(|col| col.storage.all_tags())?; + let tags: Vec<_> = tags + .into_iter() + .map(|(tag, usn)| pb::TagUsnTuple { tag, usn: usn.0 }) + .collect(); + Ok(pb::AllTagsOut { tags }) + } + + fn register_tags(&mut self, input: pb::RegisterTagsIn) -> BackendResult { + self.with_col(|col| { + col.transact(None, |col| { + let usn = if input.preserve_usn { + Usn(input.usn) + } else { + col.usn()? + }; + col.register_tags(&input.tags, usn, input.clear_first) + .map(|val| pb::Bool { val }) + }) + }) + } + + fn get_changed_tags(&mut self, input: pb::Int32) -> BackendResult { + self.with_col(|col| { + col.transact(None, |col| { + Ok(pb::GetChangedTagsOut { + tags: col.storage.get_changed_tags(Usn(input.val))?, + }) + }) + }) + } + + // config/preferences + //------------------------------------------------------------------- + + fn get_config_json(&mut self, input: pb::String) -> BackendResult { + self.with_col(|col| { + let val: Option = col.get_config_optional(input.val.as_str()); + val.ok_or(AnkiError::NotFound) + .and_then(|v| serde_json::to_vec(&v).map_err(Into::into)) + .map(Into::into) + }) + } + + fn set_config_json(&mut self, input: pb::SetConfigJsonIn) -> BackendResult { + self.with_col(|col| { + col.transact(None, |col| { + // ensure it's a well-formed object + let val: JsonValue = serde_json::from_slice(&input.val)?; + col.set_config(input.key.as_str(), &val) + }) + }) + .map(Into::into) + } + + fn remove_config(&mut self, input: pb::String) -> BackendResult { + self.with_col(|col| col.transact(None, |col| col.remove_config(input.val.as_str()))) + .map(Into::into) + } + + fn set_all_config(&mut self, input: pb::Json) -> BackendResult { + let val: HashMap = serde_json::from_slice(&input.json)?; + self.with_col(|col| { + col.transact(None, |col| { + col.storage + .set_all_config(val, col.usn()?, TimestampSecs::now()) + }) + }) + .map(Into::into) + } + + fn get_all_config(&mut self, _input: Empty) -> BackendResult { + self.with_col(|col| { + let conf = col.storage.get_all_config()?; + serde_json::to_vec(&conf).map_err(Into::into) + }) + .map(Into::into) + } + + fn get_preferences(&mut self, _input: Empty) -> BackendResult { + self.with_col(|col| col.get_preferences()) + } + + fn set_preferences(&mut self, input: pb::Preferences) -> BackendResult { + self.with_col(|col| col.transact(None, |col| col.set_preferences(input))) + .map(Into::into) + } } impl Backend { @@ -964,30 +1084,7 @@ impl Backend { &self.i18n } - /// Decode a request, process it, and return the encoded result. - pub fn run_command_bytes(&mut self, req: &[u8]) -> Vec { - let mut buf = vec![]; - - let req = match pb::BackendInput::decode(req) { - Ok(req) => req, - Err(_e) => { - // unable to decode - let err = AnkiError::invalid_input("couldn't decode backend request"); - let oerr = anki_error_to_proto_error(err, &self.i18n); - let output = pb::BackendOutput { - value: Some(oerr.into()), - }; - output.encode(&mut buf).expect("encode failed"); - return buf; - } - }; - - let resp = self.run_command(req); - resp.encode(&mut buf).expect("encode failed"); - buf - } - - pub fn run_command_bytes2( + pub fn run_command_bytes( &mut self, method: u32, input: &[u8], @@ -1016,53 +1113,6 @@ impl Backend { ) } - fn run_command(&mut self, input: pb::BackendInput) -> pb::BackendOutput { - let oval = if let Some(ival) = input.value { - match self.run_command_inner(ival) { - Ok(output) => output, - Err(err) => anki_error_to_proto_error(err, &self.i18n).into(), - } - } else { - anki_error_to_proto_error( - AnkiError::invalid_input("unrecognized backend input value"), - &self.i18n, - ) - .into() - }; - - pb::BackendOutput { value: Some(oval) } - } - - fn run_command_inner( - &mut self, - ival: pb::backend_input::Value, - ) -> Result { - use pb::backend_output::Value as OValue; - Ok(match ival { - Value::TranslateString(input) => OValue::TranslateString(self.translate_string(input)), - Value::FormatTimeSpan(input) => OValue::FormatTimeSpan(self.format_time_span(input)), - Value::AllTags(_) => OValue::AllTags(self.all_tags()?), - Value::RegisterTags(input) => OValue::RegisterTags(self.register_tags(input)?), - Value::GetChangedTags(usn) => OValue::GetChangedTags(self.get_changed_tags(usn)?), - Value::GetConfigJson(key) => OValue::GetConfigJson(self.get_config_json(&key)?), - Value::SetConfigJson(input) => OValue::SetConfigJson({ - self.set_config_json(input)?; - pb::Empty {} - }), - - Value::SetAllConfig(input) => OValue::SetConfigJson({ - self.set_all_config(&input)?; - pb::Empty {} - }), - Value::GetAllConfig(_) => OValue::GetAllConfig(self.get_all_config()?), - Value::GetPreferences(_) => OValue::GetPreferences(self.get_preferences()?), - Value::SetPreferences(prefs) => OValue::SetPreferences({ - self.set_preferences(prefs)?; - pb::Empty {} - }), - }) - } - fn fire_progress_callback(&self, progress: Progress) -> bool { if let Some(cb) = &self.progress_callback { let bytes = progress_to_proto_bytes(progress, &self.i18n); @@ -1105,126 +1155,9 @@ impl Backend { ret } - fn translate_string(&self, input: pb::TranslateStringIn) -> String { - let key = match pb::FluentString::from_i32(input.key) { - Some(key) => key, - None => return "invalid key".to_string(), - }; - - let map = input - .args - .iter() - .map(|(k, v)| (k.as_str(), translate_arg_to_fluent_val(&v))) - .collect(); - - self.i18n.trn(key, map) - } - - fn format_time_span(&self, input: pb::FormatTimeSpanIn) -> String { - let context = match pb::format_time_span_in::Context::from_i32(input.context) { - Some(context) => context, - None => return "".to_string(), - }; - match context { - pb::format_time_span_in::Context::Precise => time_span(input.seconds, &self.i18n, true), - pb::format_time_span_in::Context::Intervals => { - time_span(input.seconds, &self.i18n, false) - } - pb::format_time_span_in::Context::AnswerButtons => { - answer_button_time(input.seconds, &self.i18n) - } - } - } - pub fn db_command(&self, input: &[u8]) -> Result { self.with_col(|col| db_command_bytes(&col.storage, input)) } - - fn all_tags(&self) -> Result { - let tags = self.with_col(|col| col.storage.all_tags())?; - let tags: Vec<_> = tags - .into_iter() - .map(|(tag, usn)| pb::TagUsnTuple { tag, usn: usn.0 }) - .collect(); - Ok(pb::AllTagsOut { tags }) - } - - fn register_tags(&self, input: pb::RegisterTagsIn) -> Result { - self.with_col(|col| { - col.transact(None, |col| { - let usn = if input.preserve_usn { - Usn(input.usn) - } else { - col.usn()? - }; - col.register_tags(&input.tags, usn, input.clear_first) - }) - }) - } - - fn get_changed_tags(&self, usn: i32) -> Result { - self.with_col(|col| { - col.transact(None, |col| { - Ok(pb::GetChangedTagsOut { - tags: col.storage.get_changed_tags(Usn(usn))?, - }) - }) - }) - } - - fn get_config_json(&self, key: &str) -> Result> { - self.with_col(|col| { - let val: Option = col.get_config_optional(key); - match val { - None => Ok(vec![]), - Some(val) => Ok(serde_json::to_vec(&val)?), - } - }) - } - - fn set_config_json(&self, input: pb::SetConfigJson) -> Result<()> { - self.with_col(|col| { - col.transact(None, |col| { - if let Some(op) = input.op { - match op { - pb::set_config_json::Op::Val(val) => { - // ensure it's a well-formed object - let val: JsonValue = serde_json::from_slice(&val)?; - col.set_config(input.key.as_str(), &val) - } - pb::set_config_json::Op::Remove(_) => col.remove_config(input.key.as_str()), - } - } else { - Err(AnkiError::invalid_input("no op received")) - } - }) - }) - } - - fn set_all_config(&self, conf: &[u8]) -> Result<()> { - let val: HashMap = serde_json::from_slice(conf)?; - self.with_col(|col| { - col.transact(None, |col| { - col.storage - .set_all_config(val, col.usn()?, TimestampSecs::now()) - }) - }) - } - - fn get_all_config(&self) -> Result> { - self.with_col(|col| { - let conf = col.storage.get_all_config()?; - serde_json::to_vec(&conf).map_err(Into::into) - }) - } - - fn get_preferences(&self) -> Result { - self.with_col(|col| col.get_preferences()) - } - - fn set_preferences(&self, prefs: pb::Preferences) -> Result<()> { - self.with_col(|col| col.transact(None, |col| col.set_preferences(prefs))) - } } fn to_nids(ids: Vec) -> Vec { diff --git a/rspy/src/lib.rs b/rspy/src/lib.rs index beac28fc2..b7fd82ad2 100644 --- a/rspy/src/lib.rs +++ b/rspy/src/lib.rs @@ -93,6 +93,18 @@ fn want_release_gil(method: u32) -> bool { BackendMethod::CloseCollection => true, BackendMethod::AbortMediaSync => true, BackendMethod::BeforeUpload => true, + BackendMethod::TranslateString => false, + BackendMethod::FormatTimespan => false, + BackendMethod::RegisterTags => true, + BackendMethod::AllTags => true, + BackendMethod::GetChangedTags => true, + BackendMethod::GetConfigJson => true, + BackendMethod::SetConfigJson => true, + BackendMethod::RemoveConfig => true, + BackendMethod::SetAllConfig => true, + BackendMethod::GetAllConfig => true, + BackendMethod::GetPreferences => true, + BackendMethod::SetPreferences => true, } } else { false @@ -101,23 +113,12 @@ fn want_release_gil(method: u32) -> bool { #[pymethods] impl Backend { - fn command(&mut self, py: Python, input: &PyBytes, release_gil: bool) -> PyObject { - let in_bytes = input.as_bytes(); - let out_bytes = if release_gil { - py.allow_threads(move || self.backend.run_command_bytes(in_bytes)) - } else { - self.backend.run_command_bytes(in_bytes) - }; - let out_obj = PyBytes::new(py, &out_bytes); - out_obj.into() - } - - fn command2(&mut self, py: Python, method: u32, input: &PyBytes) -> PyResult { + fn command(&mut self, py: Python, method: u32, input: &PyBytes) -> PyResult { let in_bytes = input.as_bytes(); if want_release_gil(method) { - py.allow_threads(move || self.backend.run_command_bytes2(method, in_bytes)) + py.allow_threads(move || self.backend.run_command_bytes(method, in_bytes)) } else { - self.backend.run_command_bytes2(method, in_bytes) + self.backend.run_command_bytes(method, in_bytes) } .map(|out_bytes| { let out_obj = PyBytes::new(py, &out_bytes);